// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package template // import "miniflux.app/v2/internal/template"

import (
	"bytes"
	"embed"
	"html/template"
	"time"

	"miniflux.app/v2/internal/locale"
)

//go:embed templates/common/*.html
var commonTemplateFiles embed.FS

//go:embed templates/views/*.html
var viewTemplateFiles embed.FS

// Engine handles the templating system.
type Engine struct {
	templates map[string]*template.Template
	funcMap   *funcMap
}

// NewEngine returns a new template engine.
func NewEngine(basePath string) *Engine {
	return &Engine{
		templates: make(map[string]*template.Template),
		funcMap:   &funcMap{basePath: basePath},
	}
}

func (e *Engine) ParseTemplates() {
	funcMap := e.funcMap.Map()
	templates := map[string][]string{ // this isn't a global variable so that it can be garbage-collected.
		"about.html":               {"layout.html", "settings_menu.html"},
		"add_subscription.html":    {"feed_menu.html", "layout.html", "settings_menu.html"},
		"api_keys.html":            {"layout.html", "settings_menu.html"},
		"starred_entries.html":     {"item_meta.html", "layout.html", "pagination.html"},
		"categories.html":          {"layout.html"},
		"category_entries.html":    {"item_meta.html", "layout.html", "pagination.html"},
		"category_feeds.html":      {"feed_list.html", "layout.html"},
		"choose_subscription.html": {"feed_menu.html", "layout.html"},
		"create_api_key.html":      {"layout.html", "settings_menu.html"},
		"create_category.html":     {"layout.html"},
		"create_user.html":         {"layout.html", "settings_menu.html"},
		"edit_category.html":       {"layout.html", "settings_menu.html"},
		"edit_feed.html":           {"layout.html"},
		"edit_user.html":           {"layout.html", "settings_menu.html"},
		"entry.html":               {"layout.html"},
		"feed_entries.html":        {"item_meta.html", "layout.html", "pagination.html"},
		"feeds.html":               {"feed_list.html", "feed_menu.html", "item_meta.html", "layout.html", "pagination.html"},
		"history_entries.html":     {"item_meta.html", "layout.html", "pagination.html"},
		"import.html":              {"feed_menu.html", "layout.html"},
		"integrations.html":        {"layout.html", "settings_menu.html"},
		"login.html":               {"layout.html"},
		"offline.html":             {},
		"search.html":              {"item_meta.html", "layout.html", "pagination.html"},
		"sessions.html":            {"layout.html", "settings_menu.html"},
		"settings.html":            {"layout.html", "settings_menu.html"},
		"shared_entries.html":      {"layout.html", "pagination.html"},
		"tag_entries.html":         {"item_meta.html", "layout.html", "pagination.html"},
		"unread_entries.html":      {"item_meta.html", "layout.html", "pagination.html"},
		"users.html":               {"layout.html", "settings_menu.html"},
		"webauthn_rename.html":     {"layout.html"},
	}

	for name, dependencies := range templates {
		tpl := template.New("").Funcs(funcMap)
		for _, dependency := range dependencies {
			template.Must(tpl.ParseFS(commonTemplateFiles, "templates/common/"+dependency))
		}
		e.templates[name] = template.Must(tpl.ParseFS(viewTemplateFiles, "templates/views/"+name))
	}

	// Sanity check to ensure that all templates are correctly declared in `templates`.
	if entries, err := viewTemplateFiles.ReadDir("templates/views"); err == nil {
		for _, entry := range entries {
			if _, ok := e.templates[entry.Name()]; !ok {
				panic("Template " + entry.Name() + " isn't declared in ParseTemplates")
			}
		}
	} else {
		panic("Unable to read all embedded views templates")
	}
}

// Render process a template.
func (e *Engine) Render(name string, data map[string]any) []byte {
	tpl, ok := e.templates[name]
	if !ok {
		panic("The template " + name + " does not exists.")
	}

	// Clone the template so the per-request, language-specific functions below
	// are bound on a private copy. The shared template stored in e.templates is
	// only ever cloned (never executed directly), so concurrent requests no
	// longer race on its function map, which previously could cause a response
	// to be rendered with another concurrent request's language.
	tpl = template.Must(tpl.Clone())

	printer := locale.NewPrinter(data["language"].(string))

	// Functions that need to be declared at runtime.
	tpl.Funcs(template.FuncMap{
		"elapsed": func(timezone string, t time.Time) string {
			return elapsedTime(printer, timezone, t)
		},
		"t":      printer.Printf,
		"plural": printer.Plural,
	})

	var b bytes.Buffer
	if err := tpl.ExecuteTemplate(&b, "base", data); err != nil {
		panic(err)
	}

	return b.Bytes()
}
