package dbtable2crud

import (
	"bufio"
	"bytes"
	"fmt"
	"go/format"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/template"

	"geeks-accelerator/oss/saas-starter-kit/tools/truss/internal/goparse"
	"github.com/dustin/go-humanize/english"
	"github.com/fatih/camelcase"
	"github.com/iancoleman/strcase"
	"github.com/pkg/errors"
)

// loadTemplateObjects executes a template file based on the given model struct and
// returns the parsed go objects.
func loadTemplateObjects(log *log.Logger, model *modelDef, templateDir, filename string, tmptData map[string]interface{}) ([]*goparse.GoObject, error) {

	// Data used to execute all the of defined code sections in the template file.
	if tmptData == nil {
		tmptData = make(map[string]interface{})
	}
	tmptData["Model"] = model

	// geeks-accelerator/oss/saas-starter-kit/example-project

	// Read the template file from the local file system.
	tempFilePath := filepath.Join(templateDir, filename)
	dat, err := ioutil.ReadFile(tempFilePath)
	if err != nil {
		err = errors.WithMessagef(err, "Failed to read template file %s", tempFilePath)
		return nil, err
	}

	// New template with custom functions.
	baseTmpl := template.New("base")
	baseTmpl.Funcs(template.FuncMap{
		"Concat": func(vals ...string) string {
			return strings.Join(vals, "")
		},
		"JoinStrings": func(vals []string, sep string) string {
			return strings.Join(vals, sep)
		},
		"PrefixAndJoinStrings": func(vals []string, pre, sep string) string {
			l := []string{}
			for _, v := range vals {
				l = append(l, pre+v)
			}
			return strings.Join(l, sep)
		},
		"FmtAndJoinStrings": func(vals []string, fmtStr, sep string) string {
			l := []string{}
			for _, v := range vals {
				l = append(l, fmt.Sprintf(fmtStr, v))
			}
			return strings.Join(l, sep)
		},
		"FormatCamel": func(name string) string {
			return FormatCamel(name)
		},
		"FormatCamelTitle": func(name string) string {
			return FormatCamelTitle(name)
		},
		"FormatCamelLower": func(name string) string {
			if name == "ID" {
				return "id"
			}
			return FormatCamelLower(name)
		},
		"FormatCamelLowerTitle": func(name string) string {
			return FormatCamelLowerTitle(name)
		},
		"FormatCamelPluralTitle": func(name string) string {
			return FormatCamelPluralTitle(name)
		},
		"FormatCamelPluralTitleLower": func(name string) string {
			return FormatCamelPluralTitleLower(name)
		},
		"FormatCamelPluralCamel": func(name string) string {
			return FormatCamelPluralCamel(name)
		},
		"FormatCamelPluralLower": func(name string) string {
			return FormatCamelPluralLower(name)
		},
		"FormatCamelPluralUnderscore": func(name string) string {
			return FormatCamelPluralUnderscore(name)
		},
		"FormatCamelPluralLowerUnderscore": func(name string) string {
			return FormatCamelPluralLowerUnderscore(name)
		},
		"FormatCamelUnderscore": func(name string) string {
			return FormatCamelUnderscore(name)
		},
		"FormatCamelLowerUnderscore": func(name string) string {
			return FormatCamelLowerUnderscore(name)
		},
		"FieldTagHasOption": func(f modelField, tagName, optName string) bool {
			if f.Tags == nil {
				return false
			}
			ft, err := f.Tags.Get(tagName)
			if ft == nil || err != nil {
				return false
			}
			if ft.Name == optName || ft.HasOption(optName) {
				return true
			}
			return false
		},
		"FieldTag": func(f modelField, tagName string) string {
			if f.Tags == nil {
				return ""
			}
			ft, err := f.Tags.Get(tagName)
			if ft == nil || err != nil {
				return ""
			}
			return ft.String()
		},
		"FieldTagReplaceOrPrepend": func(f modelField, tagName, oldVal, newVal string) string {
			if f.Tags == nil {
				return ""
			}
			ft, err := f.Tags.Get(tagName)
			if ft == nil || err != nil {
				return ""
			}

			if ft.Name == oldVal || ft.Name == newVal {
				ft.Name = newVal
			} else if ft.HasOption(oldVal) {
				for idx, val := range ft.Options {
					if val == oldVal {
						ft.Options[idx] = newVal
					}
				}
			} else if !ft.HasOption(newVal) {
				if ft.Name == "" {
					ft.Name = newVal
				} else {
					ft.Options = append(ft.Options, newVal)
				}
			}

			return ft.String()
		},
		"StringListHasValue": func(list []string, val string) bool {
			for _, v := range list {
				if v == val {
					return true
				}
			}
			return false
		},
	})

	// Load the template file using the text/template package.
	tmpl, err := baseTmpl.Parse(string(dat))
	if err != nil {
		err = errors.WithMessagef(err, "Failed to parse template file %s", tempFilePath)
		log.Printf("loadTemplateObjects : %v\n%v", err, string(dat))
		return nil, err
	}

	// Generate a list of template names defined in the template file.
	tmplNames := []string{}
	for _, defTmpl := range tmpl.Templates() {
		tmplNames = append(tmplNames, defTmpl.Name())
	}

	// Stupid hack to return template names the in order they are defined in the file.
	tmplNames, err = templateFileOrderedNames(tempFilePath, tmplNames)
	if err != nil {
		return nil, err
	}

	// Loop over all the defined templates, execute using the defined data, parse the
	// formatted code and append the parsed go objects to the result list.
	var resp []*goparse.GoObject
	for _, tmplName := range tmplNames {
		// Executed the defined template with the given data.
		var tpl bytes.Buffer
		if err := tmpl.Lookup(tmplName).Execute(&tpl, tmptData); err != nil {
			err = errors.WithMessagef(err, "Failed to execute %s from template file %s", tmplName, tempFilePath)
			return resp, err
		}

		// Format the source code to ensure its valid and code to parsed consistently.
		codeBytes, err := format.Source(tpl.Bytes())
		if err != nil {
			err = errors.WithMessagef(err, "Failed to format source for %s in template file %s", tmplName, filename)

			dl := []string{}
			for idx, l := range strings.Split(tpl.String(), "\n") {
				dl = append(dl, fmt.Sprintf("%d -> ", idx)+l)
			}

			log.Printf("loadTemplateObjects : %v\n%v", err, strings.Join(dl, "\n"))
			return resp, err
		}

		// Remove extra white space from the code.
		codeStr := strings.TrimSpace(string(codeBytes))

		// Split the code into a list of strings.
		codeLines := strings.Split(codeStr, "\n")

		// Parse the code lines into a set of objects.
		objs, err := goparse.ParseLines(codeLines, 0)
		if err != nil {
			err = errors.WithMessagef(err, "Failed to parse %s in template file %s", tmplName, filename)
			log.Printf("loadTemplateObjects : %v\n%v", err, codeStr)
			return resp, err
		}

		// Append the parsed objects to the return result list.
		for _, obj := range objs.List() {
			if obj.Name == "" && obj.Type != goparse.GoObjectType_Import && obj.Type != goparse.GoObjectType_Var && obj.Type != goparse.GoObjectType_Const && obj.Type != goparse.GoObjectType_Comment && obj.Type != goparse.GoObjectType_LineBreak {
				// All objects should have a name except for multiline var/const declarations and comments.
				err = errors.Errorf("Failed to parse name with type %s from lines: %v", obj.Type, obj.Lines())
				return resp, err
			} else if string(obj.Type) == "" {
				err = errors.Errorf("Failed to parse type for %s from lines: %v", obj.Name, obj.Lines())
				return resp, err
			}

			resp = append(resp, obj)
		}
	}

	return resp, nil
}

// FormatCamel formats Valdez mountain to ValdezMountain
func FormatCamel(name string) string {
	return strcase.ToCamel(name)
}

// FormatCamelLower formats ValdezMountain to valdezmountain
func FormatCamelLower(name string) string {
	return strcase.ToLowerCamel(FormatCamel(name))
}

// FormatCamelTitle formats ValdezMountain to Valdez Mountain
func FormatCamelTitle(name string) string {
	return strings.Join(camelcase.Split(name), " ")
}

// FormatCamelLowerTitle formats ValdezMountain to valdez mountain
func FormatCamelLowerTitle(name string) string {
	return strings.ToLower(FormatCamelTitle(name))
}

// FormatCamelPluralTitle formats ValdezMountain to Valdez Mountains
func FormatCamelPluralTitle(name string) string {
	pts := camelcase.Split(name)
	lastIdx := len(pts) - 1
	pts[lastIdx] = english.PluralWord(2, pts[lastIdx], "")
	return strings.Join(pts, " ")
}

// FormatCamelPluralTitleLower formats ValdezMountain to valdez mountains
func FormatCamelPluralTitleLower(name string) string {
	return strings.ToLower(FormatCamelPluralTitle(name))
}

// FormatCamelPluralCamel formats ValdezMountain to ValdezMountains
func FormatCamelPluralCamel(name string) string {
	return strcase.ToCamel(FormatCamelPluralTitle(name))
}

// FormatCamelPluralLower formats ValdezMountain to valdezmountains
func FormatCamelPluralLower(name string) string {
	return strcase.ToLowerCamel(FormatCamelPluralTitle(name))
}

// FormatCamelPluralUnderscore formats ValdezMountain to Valdez_Mountains
func FormatCamelPluralUnderscore(name string) string {
	return strings.Replace(FormatCamelPluralTitle(name), " ", "_", -1)
}

// FormatCamelPluralLowerUnderscore formats ValdezMountain to valdez_mountains
func FormatCamelPluralLowerUnderscore(name string) string {
	return strings.ToLower(FormatCamelPluralUnderscore(name))
}

// FormatCamelUnderscore formats ValdezMountain to Valdez_Mountain
func FormatCamelUnderscore(name string) string {
	return strings.Replace(FormatCamelTitle(name), " ", "_", -1)
}

// FormatCamelLowerUnderscore formats ValdezMountain to valdez_mountain
func FormatCamelLowerUnderscore(name string) string {
	return strings.ToLower(FormatCamelUnderscore(name))
}

// templateFileOrderedNames returns the template names the in order they are defined in the file.
func templateFileOrderedNames(localPath string, names []string) (resp []string, err error) {
	file, err := os.Open(localPath)
	if err != nil {
		return resp, errors.WithStack(err)
	}
	defer file.Close()

	idxList := []int{}
	idxNames := make(map[int]string)

	idx := 0
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if !strings.HasPrefix(scanner.Text(), "{{") || !strings.Contains(scanner.Text(), "define ") {
			continue
		}

		for _, name := range names {
			if strings.Contains(scanner.Text(), "\""+name+"\"") {
				idxList = append(idxList, idx)
				idxNames[idx] = name
				break
			}
		}

		idx = idx + 1
	}

	if err := scanner.Err(); err != nil {
		return resp, errors.WithStack(err)
	}

	sort.Ints(idxList)

	for _, idx := range idxList {
		resp = append(resp, idxNames[idx])
	}

	return resp, nil
}
