// Copyright The Athenz Authors
// Licensed under the terms of the Apache version 2.0 license. See LICENSE file for terms.
// Original code based out of https://github.com/ardielle

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/ardielle/ardielle-go/rdl"
)

const RdlGoImport = "github.com/ardielle/ardielle-go/rdl"

func formatBlock(s string, leftCol int, prefix string) string {
	if s == "" {
		return ""
	}
	tab := spaces(leftCol)
	var buf bytes.Buffer
	max := 80
	col := leftCol
	lines := 1
	tokens := strings.Split(s, " ")
	for _, tok := range tokens {
		toklen := len(tok)
		if col+toklen >= max {
			buf.WriteString("\n")
			lines++
			buf.WriteString(tab)
			buf.WriteString(prefix)
			buf.WriteString(tok)
			col = leftCol + 3 + toklen
		} else {
			if col == leftCol {
				col += len(prefix)
				buf.WriteString(prefix)
			} else {
				buf.WriteString(" ")
			}
			buf.WriteString(tok)
			col += toklen + 1
		}
	}
	buf.WriteString("\n")
	emptyPrefix := strings.Trim(prefix, " ")
	pad := tab + emptyPrefix + "\n"
	return pad + buf.String() + pad
}

func formatComment(s string, leftCol int) string {
	return formatBlock(s, leftCol, "// ")
}

func spaces(count int) string {
	return stringOfChar(count, ' ')
}

func stringOfChar(count int, b byte) string {
	buf := make([]byte, 0, count)
	for i := 0; i < count; i++ {
		buf = append(buf, b)
	}
	return string(buf)
}

func addFields(reg rdl.TypeRegistry, dst []*rdl.StructFieldDef, t *rdl.Type) []*rdl.StructFieldDef {
	switch t.Variant {
	case rdl.TypeVariantStructTypeDef:
		st := t.StructTypeDef
		if st.Type != "Struct" {
			dst = addFields(reg, dst, reg.FindType(st.Type))
		}
		for _, f := range st.Fields {
			dst = append(dst, f)
		}
	}
	return dst
}

func flattenedFields(reg rdl.TypeRegistry, t *rdl.Type) []*rdl.StructFieldDef {
	return addFields(reg, make([]*rdl.StructFieldDef, 0), t)
}

func capitalize(text string) string {
	return strings.ToUpper(text[0:1]) + text[1:]
}

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

func outputWriter(outdir string, name string, ext string) (*bufio.Writer, *os.File, string, error) {
	sname := "anonymous"
	if strings.HasSuffix(outdir, ext) {
		name = filepath.Base(outdir)
		sname = name[:len(name)-len(ext)]
		outdir = filepath.Dir(outdir)
	}
	if name != "" {
		sname = name
	}
	if outdir == "" {
		return bufio.NewWriter(os.Stdout), nil, sname, nil
	}
	outfile := sname
	if !strings.HasSuffix(outfile, ext) {
		outfile += ext
	}
	path := filepath.Join(outdir, outfile)
	f, err := os.Create(path)
	if err != nil {
		return nil, nil, "", err
	}
	writer := bufio.NewWriter(f)
	return writer, f, sname, nil
}

func generationHeader(banner string) string {
	return fmt.Sprintf("//\n// This file generated by %s\n//", banner)
}

func generationPackage(schema *rdl.Schema, ns string) string {
	pkg := "main"
	if ns != "" {
		pkg = ns
	} else if schema.Name != "" {
		pkg = strings.ToLower(string(schema.Name))
	}
	return pkg
}

func goFmt(filename string) error {
	return exec.Command("go", "fmt", filename).Run()
}

func GoType(reg rdl.TypeRegistry, rdlType rdl.TypeRef, optional bool, items rdl.TypeRef, keys rdl.TypeRef, precise bool, reference bool, packageName string) string {
	rdlPrefix := "rdl."
	if reg.Name() == "rdl" {
		rdlPrefix = ""
	}
	cleanType := string(rdlType)
	if !strings.HasPrefix(cleanType, "rdl.") {
		cleanType = capitalize(strings.Replace(string(rdlType), ".", "_", -1))
	}
	prefix := ""
	if optional {
		prefix = "*"
	}
	t := reg.FindType(rdlType)
	if t.Variant == 0 {
		panic("Cannot find type '" + rdlType + "'")
	}
	lrdlType := strings.ToLower(string(rdlType))
	if precise {
		switch lrdlType {
		case "string":
			return "string"
		case "symbol":
			return rdlPrefix + "Symbol"
		case "bool", "int32", "int64", "int16", "int8", "float64", "float32":
			return prefix + strings.ToLower(cleanType)
		default:
			bt := reg.BaseType(t)
			switch bt {
			case rdl.BaseTypeString, rdl.BaseTypeSymbol:
				return cleanType
			case rdl.BaseTypeInt8, rdl.BaseTypeInt16, rdl.BaseTypeInt32, rdl.BaseTypeInt64, rdl.BaseTypeFloat32, rdl.BaseTypeFloat64, rdl.BaseTypeBool:
				return prefix + cleanType
			case rdl.BaseTypeTimestamp, rdl.BaseTypeUUID:
				fullTypeName := rdlPrefix + cleanType
				return prefix + fullTypeName
			default:
				if lrdlType == "struct" {
					fullTypeName := rdlPrefix + cleanType
					return prefix + fullTypeName
				}
			}
		}
	} else {
		switch lrdlType {
		case "timestamp":
			return prefix + rdlPrefix + "Timestamp"
		case "uuid":
			return prefix + rdlPrefix + "UUID"
		case "struct":
			return prefix + rdlPrefix + "Struct"
		}
	}
	bt := reg.BaseType(t)
	switch bt {
	case rdl.BaseTypeAny:
		return "interface{}"
	case rdl.BaseTypeString:
		return "string"
	case rdl.BaseTypeSymbol:
		return rdlPrefix + "Symbol"
	case rdl.BaseTypeBool:
		return prefix + "bool"
	case rdl.BaseTypeInt8, rdl.BaseTypeInt16, rdl.BaseTypeInt32, rdl.BaseTypeInt64, rdl.BaseTypeFloat32, rdl.BaseTypeFloat64:
		return prefix + strings.ToLower(bt.String())
	case rdl.BaseTypeArray:
		if reference {
			name := "Array"
			if t.ArrayTypeDef != nil {
				name = string(t.ArrayTypeDef.Name)
			}
			if name != "Array" {
				return name
			}
		}
		i := rdl.TypeRef("Any")
		switch t.Variant {
		case rdl.TypeVariantArrayTypeDef:
			i = t.ArrayTypeDef.Items
		default:
			if items != "" {
				i = items
			}
		}
		gitems := GoType(reg, i, false, "", "", precise, reference, packageName)
		return "[]" + gitems
	case rdl.BaseTypeMap:
		if reference {
			//we check if we have defined a type, i.e. the type name is not "Map"
			name := rdl.TypeName("Map")
			if t.MapTypeDef != nil {
				name = t.MapTypeDef.Name
			} else if t.AliasTypeDef != nil {
				name = t.AliasTypeDef.Name
			}
			if name != "Map" {
				return string(name)
			}
		}
		k := rdl.TypeRef("Any")
		i := rdl.TypeRef("Any")
		switch t.Variant {
		case rdl.TypeVariantMapTypeDef:
			k = t.MapTypeDef.Keys
			i = t.MapTypeDef.Items
		default:
			if keys != "" {
				k = keys
			}
			if items != "" {
				i = items
			}
		}
		gkeys := GoType(reg, k, false, "", "", precise, reference, packageName)
		gitems := GoType(reg, i, false, "", "", precise, reference, packageName)
		return "map[" + gkeys + "]" + gitems
	case rdl.BaseTypeStruct:
		switch t.Variant {
		case rdl.TypeVariantAliasTypeDef:
			if t.AliasTypeDef.Name == "Struct" {
				return prefix + "map[string]interface{}"
			}
		}
		if packageName != "" {
			return "*" + packageName + "." + cleanType
		}
		return "*" + cleanType
	case rdl.BaseTypeUnion:
		return "*" + cleanType
	case rdl.BaseTypeEnum:
		return prefix + cleanType
	case rdl.BaseTypeBytes:
		return "[]byte"
	default:
		return prefix + cleanType
	}
}

func SafeTypeVarName(rtype rdl.TypeRef) rdl.TypeName {
	tokens := strings.Split(string(rtype), ".")
	return rdl.TypeName(capitalize(strings.Join(tokens, "")))
}
