/*
 * Copyright 2018 darkdarkfruit.  All rights reserved.
 *
 * Use of this source code is governed by a MIT style
 * license that can be found in the LICENSE file.
 *
 */

/*
Go(Golang) template manager, especially suited for web.


# There are 2 types of templateEnv(aka: 2 types of templateName). Default is ContextMode
1. ContextMode: Name starts with "C->" or not starts with "F->"

	eg: "C->main/demo/demo.tpl.html"
	 or "C-> main/demo/demo.tpl.html" (not: blanks before file(main/demo/demo.tpl.html) will be discarded)
	 or	"main/demo/demo.tpl.html"

2. FilesMode:   Name starts with "F->". (default separator of multiple files is ";")

	eg: "F->main/demo/demo.tpl.html"
     or "F-> main/demo/demo.tpl.html"
     or "F-> main/demo/demo.tpl.html;main/demo/demo_ads.tpl.html" (will use the first file name when executing template)

# ContextMode is using template nesting, somewhat like template-inheritance in django/jinja2/...
ContextMode will load context templates, then execute template in file: `FilePathOfLayoutRelativeToRoot`.

# FilesMode is basically the same as http/template

*/
package templatemanager

// package main

import (
	"bytes"
	"fmt"
	"github.com/oxtoacart/bpool"
	"github.com/tdewolff/minify"
	"github.com/tdewolff/minify/html"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	MimeHtml          = "text/html"
	templateEngineKey = "github.com/darkdarkfruit/templatemanager"
	VERSION           = "0.7.1"
)

var (
	htmlContentType    = []string{"text/html; charset=utf-8"}
	bufpool            *bpool.BufferPool
	htmlMinifier       *minify.M
	goTemplateMinifier *minify.M
)

func init() {
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	bufpool = bpool.NewBufferPool(64)
	// log.Println("buffer allocation successful")
	htmlMinifier = minify.New()
	htmlMinifier.AddFunc(MimeHtml, html.Minify)

	goTemplateMinifier = minify.New()

	// we keep the minifier wild.
	goTemplateMinifier.Add(MimeHtml, &html.Minifier{
		KeepDefaultAttrVals: true,
		KeepWhitespace:      true,
		KeepDocumentTags:    true,
		KeepEndTags:         true,
	})
}

func Version() string {
	return VERSION
}

type TemplateManager struct {
	Config        TemplateConfig
	TemplatesMap  map[string]*template.Template
	templateMutex sync.RWMutex
}

type TemplateConfig struct {
	DirOfRoot                      string           // template root dir
	DirOfMainRelativeToRoot        string           // template dir: main
	DirOfContextRelativeToRoot     string           // template dir: context
	FilePathOfLayoutRelativeToRoot string           // template layout file path
	Extension                      string           // template extension
	FuncMap                        template.FuncMap // template functions
	Delims                         Delims           // delimiters

	IsDebugging          bool // true: Show debug info; false: disable debug info and enable cache.
	VerboseLevel         int  // 0: not show anything
	EnableMinifyTemplate bool // enable minify template after loading it and before storing it to the memory.
	EnableMinifyHtml     bool // decide to minify html while output
	ShowQps              bool // if VerboseLevel >= 1 || ShowQps { // show qps }, default is false
}

type Delims struct {
	Left  string
	Right string
}

func New(config TemplateConfig) *TemplateManager {
	return &TemplateManager{
		Config: config,

		TemplatesMap:  make(map[string]*template.Template),
		templateMutex: sync.RWMutex{},
	}
}

func NewDefault(isDebugging bool) *TemplateManager {
	return New(NewDefaultConfig(isDebugging))
}

func NewDefaultConfig(isDebugging bool) TemplateConfig {
	return TemplateConfig{
		DirOfRoot:                      "templates",
		DirOfMainRelativeToRoot:        "main",
		DirOfContextRelativeToRoot:     "context",
		FilePathOfLayoutRelativeToRoot: "context/layout/layout.tpl.html",
		Extension:                      ".html",
		FuncMap:                        make(template.FuncMap),
		Delims:                         Delims{Left: "{{", Right: "}}"},
		IsDebugging:                    isDebugging,
		VerboseLevel:                   1,
		EnableMinifyTemplate:           false,
		EnableMinifyHtml:               false,
	}
}
func (tm *TemplateManager) DoShowDebugMessage() bool {
	if tm.Config.VerboseLevel <= 0 {
		return false
	}
	return tm.Config.IsDebugging && tm.Config.VerboseLevel > 0
}

// 0: disables all
func (tm *TemplateManager) SetVerboseLevel(level int) {
	tm.Config.VerboseLevel = level
}

func (tm *TemplateManager) GetTemplateNames() (names []string) {
	for k := range tm.TemplatesMap {
		names = append(names, k)
	}
	return
}

func (tm *TemplateManager) GetFilePathOfBase() (name string) {
	return path.Join(tm.Config.DirOfRoot, tm.Config.FilePathOfLayoutRelativeToRoot)
}

func (tm *TemplateManager) GetMapOfTemplateNameToDefinedNames() (m map[string]string) {
	m = make(map[string]string)
	for k, tpl := range tm.TemplatesMap {
		m[k] = tpl.DefinedTemplates()
	}
	return
}

func (tm *TemplateManager) Report() string {
	s := fmt.Sprintf(`
Report of template manager
==============================
--> config
%#v
------------------------
--> (map(sum=%d):  templateName -> it's definedNames), 
`, tm.Config, len(tm.TemplatesMap))
	i := 0
	for tplName, definedNames := range tm.GetMapOfTemplateNameToDefinedNames() {
		i += 1
		s += fmt.Sprintf("%d: %q -> %s\n", i, tplName, definedNames)
	}
	s += "------------------------\n"
	return s
}

func (tm *TemplateManager) getDirOfMain() string {
	return path.Join(tm.Config.DirOfRoot, tm.Config.DirOfMainRelativeToRoot)
}

func (tm *TemplateManager) getDirOfContext() string {
	return path.Join(tm.Config.DirOfRoot, tm.Config.DirOfContextRelativeToRoot)
}

func getTemplateFilePathsByWalking(root string, ext string, prefix string) ([]string, error) {
	var filePaths []string
	walkFunc := func(p string, info os.FileInfo, err error) error {
		if err != nil {
			currentDir, _ := os.Getwd()
			log.Panicf("error happens while walking dir: %q(current dir is: %q), walking-root-for-function: %q,  err: %v", p, currentDir, root, err)
		}
		if !info.IsDir() && path.Ext(p) == ext {
			filePaths = append(filePaths, path.Join(prefix, p))
		}
		return nil
	}
	err := filepath.Walk(root, walkFunc)
	if err != nil {
		log.Printf("Faild walking root dir: %q. err: %q", root, err)
		log.Fatalf("Faild walking root dir: %q. err: %q", root, err)
		return nil, err
	}
	return filePaths, nil
}

// ContainsString checks if the slice has the contains value in it.
func ContainsString(slice []string, contains string) bool {
	for _, value := range slice {
		if value == contains {
			return true
		}
	}
	return false
}

func (tm *TemplateManager) getContextFiles() []string {
	contextFiles, err := getTemplateFilePathsByWalking(tm.getDirOfContext(), tm.Config.Extension, "")
	if err != nil {
		log.Fatalf("Could not get context files of dir: %q. err: %s", tm.getDirOfContext(), err)
	}
	if tm.DoShowDebugMessage() {
		log.Printf("ContextFiles are: %v", contextFiles)
	}
	if !ContainsString(contextFiles, tm.GetFilePathOfBase()) {
		contextFiles = append(contextFiles, tm.GetFilePathOfBase())
	}

	return contextFiles
}

// get templates which is not context file.
func (tm *TemplateManager) getMainFiles() []string {
	// mainFiles, err := filepath.Glob(path.Join(tm.getDirOfMain(), "**", "*"+tm.Config.Extension))
	mainFiles, err := getTemplateFilePathsByWalking(tm.getDirOfMain(), tm.Config.Extension, "")
	if err != nil {
		log.Fatalf("Could not get main files of dir: %q. err: %s", tm.getDirOfMain(), err)
	}

	// DirOfContextRelativeToRoot might be a sub directory of DirOfMainRelativeToRoot
	var mf []string
	for _, f := range mainFiles {
		// skip context files (if context_dir is a sub_dir of main_dir)
		if strings.HasPrefix(f, tm.Config.DirOfContextRelativeToRoot) {
			continue
		}
		mf = append(mf, f)
	}

	log.Printf("Found %d main templates(exclude context templates)", len(mf))
	return mf
}

func (tm *TemplateManager) getBasicTemplateNameByFilePath(filepath string) string {
	s := strings.TrimPrefix(filepath, tm.Config.DirOfRoot)
	return strings.TrimPrefix(s, "/")
}

// func (tm *TemplateManager) getContextTemplate() *template.Template {
//	contextTemplate := template.Must(template.ParseFiles(tm.getContextFiles()...))
//	return contextTemplate
//}

func (tm *TemplateManager) setTemplate(te *TemplateEnv, tpl *template.Template) {
	if tpl == nil {
		panic("Template can not be nil")
	}

	tplName := te.StandardTemplateName()
	tm.templateMutex.Lock()
	defer tm.templateMutex.Unlock()
	tm.TemplatesMap[tplName] = tpl
}

func (tm *TemplateManager) parseMainFiles() error {
	for i, f := range tm.getMainFiles() {
		if tm.DoShowDebugMessage() {
			log.Printf("\n")
			log.Printf("--(template: seq: %d)--> Parsing template file: %q", i, f)
		}
		tm.parseMainTemplateByFilePath(f)
	}
	log.Printf("")
	return nil
}

func (tm *TemplateManager) MustTemplate(tplName string, filesForParsing []string) *template.Template {
	if !tm.Config.EnableMinifyTemplate {
		tpl := template.Must(template.New(tplName).Funcs(tm.Config.FuncMap).ParseFiles(filesForParsing...))
		return tpl
	} else {
		/* Could not find a better way to store minified content while keeps filenames which required by template to "ParseFiles"
		 * solution:
		 * 	create a tmp dir, then populates it with minified files.
		 * ...
		 * ...
		 */
		if tm.DoShowDebugMessage() {
			log.Printf("Minifying template: %q", tplName)
		}
		tmpDir, err := ioutil.TempDir("", "go-template")
		if err != nil {
			log.Printf("Could not create temparary dir. err: %s", err)
			panic(err)
			return nil
		}
		defer func() {
			err := os.RemoveAll(tmpDir)
			if err != nil {
				log.Printf("could not remove tmpDir: %q. e: %v", tmpDir, err)
			}
		}()

		for _, f := range filesForParsing {
			baseName := path.Base(f)
			fpath := path.Join(tmpDir, baseName)
			outFile, err := os.Create(fpath)
			if err != nil {
				fmt.Printf("Could not create file: %q. err: %s", fpath, err)
				panic(err)
				return nil
			}

			inFile, err := os.Open(f)
			if err != nil {
				fmt.Printf("Could not open file: %q. err: %s", f, err)
				panic(err)
				return nil
			}
			err = goTemplateMinifier.Minify(MimeHtml, outFile, inFile)
			if err != nil {
				log.Printf("Could not minify template in buf. err: %s", err)
				panic(err)
			}
		}
		tpl := template.Must(template.New(tplName).Funcs(tm.Config.FuncMap).ParseGlob(filepath.Join(tmpDir, "*")))
		return tpl
	}
}

func (tm *TemplateManager) ParseContextModeTemplate(te *TemplateEnv) *template.Template {
	if !te.IsContextMode() {
		return nil
	}

	tplName := te.StandardTemplateName()
	filePaths := te.GetFilePaths(tm.Config.DirOfRoot)

	if tm.DoShowDebugMessage() {
		if len(filePaths) == 1 {
			log.Printf("ContextEnv Parsing: (tplName -> tplPath) (%q -> %q)", tplName, filePaths[0])
		} else {
			log.Printf("ContextEnv Parsing: (tplName -> tplPaths) (%q -> %q)", tplName, filePaths)
		}
	}
	contextFiles := tm.getContextFiles()
	filesForParsing := append(contextFiles, filePaths...)

	// tpl := template.Must(template.New(tplName).Funcs(tm.Config.FuncMap).ParseFiles(filesForParsing...))
	tpl := tm.MustTemplate(tplName, filesForParsing)
	tm.setTemplate(te, tpl)
	if tm.DoShowDebugMessage() {
		log.Printf("ContextEnv template:     (templateName -> definedTemplates): %q -> %s", tpl.Name(), tpl.DefinedTemplates())
	}
	return tpl
}

func (tm *TemplateManager) ParseFilesModeTemplate(te *TemplateEnv) *template.Template {
	if !te.IsFilesMode() {
		return nil
	}
	tplName := te.StandardTemplateName()
	filesForParsing := te.GetFilePaths(tm.Config.DirOfRoot)
	if tm.DoShowDebugMessage() {
		log.Printf("FilesEnv Parsing: (tplName -> tplPath) (%q -> %q)", tplName, filesForParsing)
	}
	// tpl := template.Must(template.New(tplName).Funcs(tm.Config.FuncMap).ParseFiles(filesForParsing...))
	tpl := tm.MustTemplate(tplName, filesForParsing)
	tm.setTemplate(te, tpl)
	if tm.DoShowDebugMessage() {
		log.Printf("FilesEnv template: (tplName -> definedTemplates): %q -> %s", tpl.Name(), tpl.DefinedTemplates())
	}
	return tpl
}

func (tm *TemplateManager) parseTemplate(te *TemplateEnv) *template.Template {
	tplName := te.StandardTemplateName()
	if te.IsContextMode() {
		if tm.DoShowDebugMessage() {
			log.Printf("tplName: %q is a contextEnv tplName", tplName)
		}
		return tm.ParseContextModeTemplate(te)
	} else if te.IsFilesMode() {
		if tm.DoShowDebugMessage() {
			log.Printf("tplName: %q is a filesEnv tplName", tplName)
		}
		return tm.ParseFilesModeTemplate(te)
	} else {
		log.Printf("tplName: %q is an invalid tplName", tplName)
		msg := fmt.Sprintf("Could not find template by tplName: %q", tplName)
		log.Printf(msg)
		panic(msg)
	}
}

func (tm *TemplateManager) parseMainTemplateByFilePath(filePath string) *template.Template {
	basicTplName := tm.getBasicTemplateNameByFilePath(filePath)
	te := NewTemplateEnvByParsing(basicTplName)
	te.ToContextMode()
	contextTpl := tm.ParseContextModeTemplate(te)
	if contextTpl == nil {
		log.Printf("Error filepath: %q", filePath)
	}

	te.ToFilesMode()
	filesTpl := tm.ParseFilesModeTemplate(te)
	if filesTpl == nil {
		log.Printf("Error filepath: %q", filePath)
	}
	return contextTpl
}

func (tm *TemplateManager) Init(useMaster bool) error {
	log.Printf("Initing templates. DirOfMainRelativeToRoot: %q, DirOfContextRelativeToRoot: %q", tm.Config.DirOfMainRelativeToRoot, tm.Config.DirOfContextRelativeToRoot)
	includeFunc := func(name string, data interface{}) (template.HTML, error) {
		buf := new(bytes.Buffer)
		err := tm.ExecuteTemplate(buf, name, data)
		return template.HTML(buf.String()), err
	}
	tm.Config.FuncMap["include"] = includeFunc

	return tm.parseMainFiles()
}

func (tm *TemplateManager) GetTemplate(tplName string) (*template.Template, bool) {
	tm.templateMutex.RLock()
	defer tm.templateMutex.RUnlock()
	tpl, ok := tm.TemplatesMap[tplName]
	return tpl, ok
}

func (tm *TemplateManager) rightBeforeExecuteTemplate(tpl *template.Template, out io.Writer, name string, data interface{}) error {
	if !tm.Config.EnableMinifyHtml {
		return tpl.ExecuteTemplate(out, name, data)
	} else {
		buf := bufpool.Get()
		defer bufpool.Put(buf)

		err := tpl.ExecuteTemplate(buf, name, data)
		if err != nil {
			log.Printf("Error executing template of %q with data: %v", name, data)
			return err
		}

		if err := htmlMinifier.Minify(MimeHtml, out, buf); err != nil {
			log.Printf("Error while minifying text/html. err: %s", err)
			return err
		}
		//buf.WriteTo(out)
		return nil
		//return tm.ExecuteTemplate(out, name, data)
	}
}

func (tm *TemplateManager) ExecuteTemplate(out io.Writer, templateName string, data interface{}) error {
	t0 := time.Now()
	var tpl *template.Template
	var err error
	var ok bool

	te := NewTemplateEnvByParsing(templateName)
	tplName := te.StandardTemplateName()
	if tm.DoShowDebugMessage() {
		log.Printf("Request executing template name: %q, standard template name is: %q", templateName, tplName)
	}
	tpl, ok = tm.GetTemplate(tplName)

	if !ok || tm.Config.IsDebugging {
		log.Printf("Template-not-exist or in-debug-mode. Requst executing templateName: %q. Re-parsing it.", tplName)
		tpl = tm.parseTemplate(te)
		tpl, ok = tm.GetTemplate(tplName)
		if !ok {
			log.Printf("Could not find correspondent template by tplName: %s", tplName)
		}
	}

	var name string
	if te.IsContextMode() {
		name = filepath.Base(tm.Config.FilePathOfLayoutRelativeToRoot)
	} else if te.IsFilesMode() {
		name = filepath.Base(te.Names[0])
	}

	err = tm.rightBeforeExecuteTemplate(tpl, out, name, data)
	if err != nil {
		log.Printf("TemplateManager execute template error: %s", err)
		return err
	}

	//// render
	//if te.IsContextMode() {
	//	//err = tpl.ExecuteTemplate(out, filepath.Base(tm.Config.FilePathOfLayoutRelativeToRoot), data)
	//	err = tm.rightBeforeExecuteTemplate(tpl, out, filepath.Base(tm.Config.FilePathOfLayoutRelativeToRoot), data)
	//	if err != nil {
	//		log.Printf("TemplateManager execute template error: %s", err)
	//		return err
	//	}
	//} else if te.IsFilesMode() {
	//	name := filepath.Base(te.Names[0])
	//	err = tpl.ExecuteTemplate(out, name, data)
	//	if err != nil {
	//		log.Printf("TemplateManager execute template error: %s", err)
	//		return err
	//	}
	//}

	if tm.Config.VerboseLevel >= 1 || tm.Config.ShowQps {
		log.Printf("func: ExecuteTemplate, name: %q, isDebuging: %v, stat: %s", templateName, tm.Config.IsDebugging, NewQpsStat(t0, time.Now(), 1).ShortString())
	}
	return nil
}