package internal

import (
	"fmt"
	"go/types"
	"io"
	"path/filepath"
	"strings"

	. "github.com/dave/jennifer/jen"
	"github.com/friendsofgo/errors"
)

func Generate(m *StructMapping, goPackage string, goFile string, w io.Writer) (outputFilename string, err error) {
	f := NewFile(goPackage)

	f.PackageComment("Code generated by construct, DO NOT EDIT.")

	f.ImportAlias("github.com/networkteam/construct/json", "cjson")

	generateConstSchema(f, m)

	generateSortFields(f, m)

	changeSetName, err := generateChangeSetStruct(f, m)
	if err != nil {
		return "", errors.Wrap(err, "generating ChangeSet struct")
	}

	var toMapBlock []Code
	toMapBlock = append(toMapBlock, Id("m").Op(":=").Make(Map(String()).Interface()))

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {
			fieldName := firstToUpper(fm.Name)

			var prepareStmt *Statement
			if fm.WriteColDef.ToJSON {
				prepareStmt = Id("data").Op(",").Id("_").Op(":=").Qual("encoding/json", "Marshal").Call(Id("c").Dot(fieldName))
			}

			mapAssign := Id("m").Index(Lit(fm.WriteColDef.Col)).Op("=")
			if fm.WriteColDef.ToJSON {
				mapAssign.Id("data")
			} else if _, ok := fm.FieldType.(*types.Slice); ok {

				mapAssign.Id("c").Dot(fieldName)
			} else {
				mapAssign.Op("*").Id("c").Dot(fieldName)
			}
			code := If(Id("c").Dot(fieldName).Op("!=").Nil()).Block(prepareStmt, mapAssign)
			toMapBlock = append(toMapBlock, code)
		}
	}

	toMapBlock = append(toMapBlock, Return(Id("m")))

	f.Func().Params(
		Id("c").Id(changeSetName),
	).Id("toMap").Params().Map(String()).Interface().Block(
		toMapBlock...,
	).Line()

	var toChangeSetBlock []Code

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {

			code := Id("c").Dot(firstToUpper(fm.Name)).Op("=").
				Op("&").Id("r").Dot(firstToUpper(fm.Name))

			switch v := fm.FieldType.(type) {
			case *types.Slice:

				code = Id("c").Dot(firstToUpper(fm.Name)).Op("=").Id("r").Dot(firstToUpper(fm.Name))
			case *types.Named:
				path := v.Obj().Pkg().Path()
				name := v.Obj().Name()
				fqdnType := path + "." + name

				switch fqdnType {
				case "github.com/gofrs/uuid.UUID":
					code = If(Id("r").Dot(firstToUpper(fm.Name)).Op("!=").Qual(path, "Nil")).Block(
						code.Clone(),
					)
				case "time.Time":
					code = If(Op("!").Id("r").Dot(firstToUpper(fm.Name)).Dot("IsZero").Call()).Block(
						code.Clone(),
					)
				}
			}

			toChangeSetBlock = append(toChangeSetBlock, code)
		}
	}

	toChangeSetBlock = append(toChangeSetBlock, Return())

	f.Func().Id(firstToUpper(m.TargetName) + "ToChangeSet").Params(
		Id("r").Qual(m.MappingTypePackage, m.MappingTypeName),
	).Params(Id("c").Id(changeSetName)).Block(
		toChangeSetBlock...,
	).Line()

	generateDefaultSelectJsonObject(f, m)

	generateScanJsonRow(f, m)

	ext := filepath.Ext(goFile)
	baseFilename := goFile[0 : len(goFile)-len(ext)]

	outputFilename = baseFilename + "_" + strings.ToLower(m.MappingTypeName) + "_gen.go"
	return outputFilename, f.Render(w)
}

func generateConstSchema(f *File, m *StructMapping) {
	f.Const().DefsFunc(func(g *Group) {
		for _, fm := range m.FieldMappings {
			if fm.ReadColDef != nil {
				g.Id(readColConstId(m, fm)).Op("=").Lit(fm.ReadColDef.Col)
			}
		}
	})
}

func readColConstId(m *StructMapping, fm FieldMapping) string {
	return firstToLower(m.MappingTypeName) + "_" + firstToLower(fm.Name)
}

func generateDefaultSelectJsonObject(f *File, m *StructMapping) {
	varName := firstToLower(m.TargetName + "DefaultSelectJson")

	code := Qual("github.com/networkteam/construct/json", "JsonBuildObject").Call()
	for _, fm := range m.FieldMappings {
		if fm.ReadColDef != nil {
			setExpValue := Lit(fm.ReadColDef.Col)

			if v, ok := fm.FieldType.(*types.Slice); ok {
				if v, ok := v.Elem().(*types.Basic); ok && v.Kind() == types.Byte {
					setExpValue = Lit("ENCODE(" + fm.ReadColDef.Col + "," + "'BASE64')")
				}
			}
			code.Op(".").Line().Id("Set").Call(Lit(fm.Name), Qual("github.com/networkteam/construct/json", "Exp").Call(setExpValue))
		}
	}

	f.Var().Id(varName).Op("=").Add(code).Line()
}

func generateScanJsonRow(f *File, m *StructMapping) {
	f.Func().Id(firstToLower(m.TargetName)+"ScanJsonRow").Params(
		Id("row").Qual("github.com/networkteam/construct", "RowScanner"),
	).Params(
		Id("result").Qual(m.MappingTypePackage, m.MappingTypeName),
		Id("err").Error(),
	).Block(
		Var().Id("data").Op("[]").Byte(),
		If(
			Id("err").Op(":=").Id("row").Dot("Scan").Call(Op("&").Id("data")),
			Id("err").Op("!=").Nil(),
		).Block(
			If(Id("err").Op("==").Qual("database/sql", "ErrNoRows")).Block(
				Return(Id("result"), Qual("github.com/networkteam/construct", "ErrNotFound")),
			),
			Return(Id("result"), Id("err")),
		),
		Return(Id("result"), Qual("encoding/json", "Unmarshal").Call(Id("data"), Op("&").Id("result"))),
	)
}

func generateChangeSetStruct(f *File, m *StructMapping) (changeSetName string, err error) {
	var structFields []Code

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {
			code := Id(firstToUpper(fm.Name))
			switch v := fm.FieldType.(type) {
			case *types.Basic:
				code.Op("*").Id(v.String())
			case *types.Named:
				typeName := v.Obj()
				code.Op("*").Qual(
					typeName.Pkg().Path(),
					typeName.Name(),
				)
			case *types.Pointer:
				code.Op("*")
				elemType := v.Elem()
				switch v := elemType.(type) {
				case *types.Basic:
					code.Op("*").Id(v.String())
				case *types.Named:
					typeName := v.Obj()
					code.Op("*").Qual(
						typeName.Pkg().Path(),
						typeName.Name(),
					)
				case *types.Slice:
					return "", fmt.Errorf("pointer to slice is not supported")
				default:
					return "", fmt.Errorf("pointer type not handled: %T", v)
				}
			case *types.Slice:
				code.Op("[]")
				elemType := v.Elem()
				switch v := elemType.(type) {
				case *types.Basic:
					code.Id(v.String())
				case *types.Named:
					typeName := v.Obj()
					code.Qual(
						typeName.Pkg().Path(),
						typeName.Name(),
					)
				default:
					return "", fmt.Errorf("slice type not handled: %T", v)
				}
			default:
				return "", fmt.Errorf("struct field type not handled: %T (for %s)", v, fm.FieldType)
			}
			structFields = append(structFields, code)
		}
	}

	changeSetName = firstToUpper(m.TargetName + "ChangeSet")
	f.Type().Id(changeSetName).Struct(structFields...)

	return changeSetName, nil
}

func generateSortFields(f *File, m *StructMapping) {
	sortFieldDict := make(Dict)
	for _, fm := range m.FieldMappings {
		if fm.ReadColDef != nil && fm.ReadColDef.Sortable {
			sortFieldDict[Lit(strings.ToLower(fm.Name))] = Id(readColConstId(m, fm))
		}
	}
	f.Var().Id(firstToLower(m.TargetName + "SortFields")).Op("=").Map(String()).String().
		Values(sortFieldDict)
}
package internal

import (
	"fmt"
	"go/types"
	"io"
	"path/filepath"
	"strings"

	. "github.com/dave/jennifer/jen"
	"github.com/friendsofgo/errors"
)

// Generate Go code for the struct mapping
func Generate(m *StructMapping, goPackage string, goFile string, w io.Writer) (outputFilename string, err error) {
	f := NewFile(goPackage)

	f.PackageComment("Code generated by construct, DO NOT EDIT.")

	f.ImportAlias("github.com/networkteam/construct/json", "cjson")

	generateConstSchema(f, m)

	generateSortFields(f, m)

	// ChangeSet struct

	changeSetName, err := generateChangeSetStruct(f, m)
	if err != nil {
		return "", errors.Wrap(err, "generating ChangeSet struct")
	}

	// toMap() method for ChangeSet

	var toMapBlock []Code
	toMapBlock = append(toMapBlock, Id("m").Op(":=").Make(Map(String()).Interface()))

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {
			fieldName := firstToUpper(fm.Name)

			var prepareStmt *Statement
			if fm.WriteColDef.ToJSON {
				prepareStmt = Id("data").Op(",").Id("_").Op(":=").Qual("encoding/json", "Marshal").Call(Id("c").Dot(fieldName))
			}

			mapAssign := Id("m").Index(Lit(fm.WriteColDef.Col)).Op("=")
			if fm.WriteColDef.ToJSON {
				mapAssign.Id("data")
			} else if _, ok := fm.FieldType.(*types.Slice); ok {
				// Do not indirect slice values
				mapAssign.Id("c").Dot(fieldName)
			} else {
				mapAssign.Op("*").Id("c").Dot(fieldName)
			}
			code := If(Id("c").Dot(fieldName).Op("!=").Nil()).Block(prepareStmt, mapAssign)
			toMapBlock = append(toMapBlock, code)
		}
	}

	toMapBlock = append(toMapBlock, Return(Id("m")))

	f.Func().Params(
		Id("c").Id(changeSetName),
	).Id("toMap").Params().Map(String()).Interface().Block(
		toMapBlock...,
	).Line()

	// myRecordToChangeSet() function

	var toChangeSetBlock []Code

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {
			// This is the default of taking the address of the field value
			code := Id("c").Dot(firstToUpper(fm.Name)).Op("=").
				Op("&").Id("r").Dot(firstToUpper(fm.Name))

			switch v := fm.FieldType.(type) {
			case *types.Slice:
				// Do not take address of slice values
				code = Id("c").Dot(firstToUpper(fm.Name)).Op("=").Id("r").Dot(firstToUpper(fm.Name))
			case *types.Named:
				path := v.Obj().Pkg().Path()
				name := v.Obj().Name()
				fqdnType := path + "." + name
				// Special handling to check for empty values (e.g. UUID and Time) before adding them to the changeset
				switch fqdnType {
				case "github.com/gofrs/uuid.UUID":
					code = If(Id("r").Dot(firstToUpper(fm.Name)).Op("!=").Qual(path, "Nil")).Block(
						code.Clone(),
					)
				case "time.Time":
					code = If(Op("!").Id("r").Dot(firstToUpper(fm.Name)).Dot("IsZero").Call()).Block(
						code.Clone(),
					)
				}
			}

			toChangeSetBlock = append(toChangeSetBlock, code)
		}
	}

	toChangeSetBlock = append(toChangeSetBlock, Return())

	f.Func().Id(firstToUpper(m.TargetName) + "ToChangeSet").Params(
		Id("r").Qual(m.MappingTypePackage, m.MappingTypeName),
	).Params(Id("c").Id(changeSetName)).Block(
		toChangeSetBlock...,
	).Line()

	generateDefaultSelectJsonObject(f, m)

	generateScanJsonRow(f, m)

	ext := filepath.Ext(goFile)
	baseFilename := goFile[0 : len(goFile)-len(ext)]

	outputFilename = baseFilename + "_" + strings.ToLower(m.MappingTypeName) + "_gen.go"
	return outputFilename, f.Render(w)
}

func generateConstSchema(f *File, m *StructMapping) {
	f.Const().DefsFunc(func(g *Group) {
		for _, fm := range m.FieldMappings {
			if fm.ReadColDef != nil {
				g.Id(readColConstId(m, fm)).Op("=").Lit(fm.ReadColDef.Col)
			}
		}
	})
}

func readColConstId(m *StructMapping, fm FieldMapping) string {
	return firstToLower(m.MappingTypeName) + "_" + firstToLower(fm.Name)
}

func generateDefaultSelectJsonObject(f *File, m *StructMapping) {
	varName := firstToLower(m.TargetName + "DefaultSelectJson")

	code := Qual("github.com/networkteam/construct/json", "JsonBuildObject").Call()
	for _, fm := range m.FieldMappings {
		if fm.ReadColDef != nil {
			setExpValue := Lit(fm.ReadColDef.Col)
			// Special handling for []byte (needs encoding to Base64 (used by encoding/json for unmarshal)
			if v, ok := fm.FieldType.(*types.Slice); ok {
				if v, ok := v.Elem().(*types.Basic); ok && v.Kind() == types.Byte {
					setExpValue = Lit("ENCODE(" + fm.ReadColDef.Col + "," + "'BASE64')")
				}
			}
			code.Op(".").Line().Id("Set").Call(Lit(fm.Name), Qual("github.com/networkteam/construct/json", "Exp").Call(setExpValue))
		}
	}

	f.Var().Id(varName).Op("=").Add(code).Line()
}

func generateScanJsonRow(f *File, m *StructMapping) {
	f.Func().Id(firstToLower(m.TargetName)+"ScanJsonRow").Params(
		Id("row").Qual("github.com/networkteam/construct", "RowScanner"),
	).Params(
		Id("result").Qual(m.MappingTypePackage, m.MappingTypeName),
		Id("err").Error(),
	).Block(
		Var().Id("data").Op("[]").Byte(),
		If(
			Id("err").Op(":=").Id("row").Dot("Scan").Call(Op("&").Id("data")),
			Id("err").Op("!=").Nil(),
		).Block(
			If(Id("err").Op("==").Qual("database/sql", "ErrNoRows")).Block(
				Return(Id("result"), Qual("github.com/networkteam/construct", "ErrNotFound")),
			),
			Return(Id("result"), Id("err")),
		),
		Return(Id("result"), Qual("encoding/json", "Unmarshal").Call(Id("data"), Op("&").Id("result"))),
	)
}

func generateChangeSetStruct(f *File, m *StructMapping) (changeSetName string, err error) {
	var structFields []Code

	for _, fm := range m.FieldMappings {
		if fm.WriteColDef != nil {
			code := Id(firstToUpper(fm.Name))
			switch v := fm.FieldType.(type) {
			case *types.Basic:
				code.Op("*").Id(v.String())
			case *types.Named:
				typeName := v.Obj()
				code.Op("*").Qual(
					typeName.Pkg().Path(),
					typeName.Name(),
				)
			case *types.Pointer:
				code.Op("*")
				elemType := v.Elem()
				switch v := elemType.(type) {
				case *types.Basic:
					code.Op("*").Id(v.String())
				case *types.Named:
					typeName := v.Obj()
					code.Op("*").Qual(
						typeName.Pkg().Path(),
						typeName.Name(),
					)
				case *types.Slice:
					return "", fmt.Errorf("pointer to slice is not supported")
				default:
					return "", fmt.Errorf("pointer type not handled: %T", v)
				}
			case *types.Slice:
				code.Op("[]")
				elemType := v.Elem()
				switch v := elemType.(type) {
				case *types.Basic:
					code.Id(v.String())
				case *types.Named:
					typeName := v.Obj()
					code.Qual(
						typeName.Pkg().Path(),
						typeName.Name(),
					)
				default:
					return "", fmt.Errorf("slice type not handled: %T", v)
				}
			default:
				return "", fmt.Errorf("struct field type not handled: %T (for %s)", v, fm.FieldType)
			}
			structFields = append(structFields, code)
		}
	}

	changeSetName = firstToUpper(m.TargetName + "ChangeSet")
	f.Type().Id(changeSetName).Struct(structFields...)

	return changeSetName, nil
}

func generateSortFields(f *File, m *StructMapping) {
	sortFieldDict := make(Dict)
	for _, fm := range m.FieldMappings {
		if fm.ReadColDef != nil && fm.ReadColDef.Sortable {
			sortFieldDict[Lit(strings.ToLower(fm.Name))] = Id(readColConstId(m, fm))
		}
	}
	f.Var().Id(firstToLower(m.TargetName + "SortFields")).Op("=").Map(String()).String().
		Values(sortFieldDict)
}
