package tplimpl

import (
	"bytes"
	"context"
	"embed"
	"fmt"
	"io"
	"io/fs"
	"iter"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gohugoio/hugo/common/herrors"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugofs/files"
	"github.com/gohugoio/hugo/hugolib/doctree"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/media"
	"github.com/gohugoio/hugo/metrics"
	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/resources/kinds"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/tpl"
	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
	"github.com/spf13/afero"
)

const (
	CategoryLayout Category = iota + 1
	CategoryBaseof
	CategoryMarkup
	CategoryShortcode
	CategoryPartial
	// Internal categories
	CategoryServer
	CategoryHugo
)

const (
	SubCategoryMain     SubCategory = iota
	SubCategoryEmbedded             // Internal Hugo templates
	SubCategoryInline               // Inline partials
)

const (
	containerMarkup          = "_markup"
	containerShortcodes      = "_shortcodes"
	shortcodesPathIdentifier = "/_shortcodes/"
	containerPartials        = "_partials"
)

const (
	layoutAll    = "all"
	layoutList   = "list"
	layoutSingle = "single"
)

var (
	_ identity.IdentityProvider             = (*TemplInfo)(nil)
	_ identity.IsProbablyDependentProvider  = (*TemplInfo)(nil)
	_ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
)

const (
	processingStateInitial processingState = iota
	processingStateTransformed
)

// The identifiers may be truncated in the log, e.g.
// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
// We need this to identify position in templates with base templates applied.
var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)

var weightNoMatch = weight{w1: -1}

//
//go:embed all:embedded/templates/*
var embeddedTemplatesFs embed.FS

func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
	html, ok := opts.OutputFormats.GetByName("html")
	if !ok {
		panic("HTML output format not found")
	}
	s := &TemplateStore{
		opts:                opts,
		siteOpts:            siteOpts,
		optsOrig:            opts,
		siteOptsOrig:        siteOpts,
		htmlFormat:          html,
		storeSite:           configureSiteStorage(siteOpts, opts.Watching),
		treeMain:            doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
		treeShortcodes:      doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
		templatesByPath:     maps.NewCache[string, *TemplInfo](),
		cacheLookupPartials: maps.NewCache[string, *TemplInfo](),

		// Note that the funcs passed below is just for name validation.
		tns: newTemplateNamespace(siteOpts.TemplateFuncs),

		dh: descriptorHandler{
			opts: opts,
		},
	}

	if err := s.init(); err != nil {
		return nil, err
	}
	if err := s.insertTemplates(nil, false); err != nil {
		return nil, err
	}
	if err := s.insertEmbedded(); err != nil {
		return nil, err
	}
	if err := s.parseTemplates(); err != nil {
		return nil, err
	}
	if err := s.extractInlinePartials(); err != nil {
		return nil, err
	}
	if err := s.transformTemplates(); err != nil {
		return nil, err
	}
	if err := s.tns.createPrototypes(true); err != nil {
		return nil, err
	}
	if err := s.prepareTemplates(); err != nil {
		return nil, err
	}
	return s, nil
}

//go:generate stringer -type Category

type Category int

type SiteOptions struct {
	Site          page.Site
	TemplateFuncs map[string]any
}

type StoreOptions struct {
	// The filesystem to use.
	Fs afero.Fs

	// The logger to use.
	Log loggers.Logger

	// The path parser to use.
	PathParser *paths.PathParser

	// Set when --enableTemplateMetrics is set.
	Metrics metrics.Provider

	// All configured output formats.
	OutputFormats output.Formats

	// All configured media types.
	MediaTypes media.Types

	// The default content language.
	DefaultContentLanguage string

	// The default output format.
	DefaultOutputFormat string

	// Taxonomy config.
	TaxonomySingularPlural map[string]string

	// Whether we are in watch or server mode.
	Watching bool

	// compiled.
	legacyMappingTaxonomy map[string]legacyOrdinalMapping
	legacyMappingTerm     map[string]legacyOrdinalMapping
	legacyMappingSection  map[string]legacyOrdinalMapping
}

//go:generate stringer -type SubCategory

type SubCategory int

type TemplInfo struct {
	// The category of this template.
	category Category

	subCategory SubCategory

	// PathInfo info.
	PathInfo *paths.Path

	// Set when backed by a file.
	Fi hugofs.FileMetaInfo

	// The template content with any leading BOM removed.
	content string

	// The parsed template.
	// Note that any baseof template will be applied later.
	Template tpl.Template

	// If no baseof is needed, this will be set to true.
	// E.g. shortcode templates do not need a baseof.
	noBaseOf bool

	// If NoBaseOf is false, we will look for the final template in this tree.
	baseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]

	// The template variants that are based on this template.
	overlays []*TemplInfo

	// The base template used, if any.
	base *TemplInfo

	// The descriptior that this template represents.
	D TemplateDescriptor

	// Parser state.
	ParseInfo ParseInfo

	// The execution counter for this template.
	executionCounter atomic.Uint64

	// processing state.
	state          processingState
	isLegacyMapped bool
}

func (ti *TemplInfo) SubCategory() SubCategory {
	return ti.subCategory
}

func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
	return func(yield func(*TemplWithBaseApplied) bool) {
		ti.baseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
			for _, vv := range v {
				if !yield(vv) {
					return true, nil
				}
			}
			return false, nil
		})
	}
}

func (t *TemplInfo) IdentifierBase() string {
	if t.PathInfo == nil {
		return t.Name()
	}
	return t.PathInfo.IdentifierBase()
}

func (t *TemplInfo) GetIdentity() identity.Identity {
	return t
}

func (ti *TemplInfo) Name() string {
	if ti.Template == nil {
		if ti.PathInfo != nil {
			return ti.PathInfo.PathNoLeadingSlash()
		}
	}
	return ti.Template.Name()
}

func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
	return ti.Template.Prepare()
}

func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
	return t.isProbablyTheSameIDAs(other)
}

func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
	for _, overlay := range t.overlays {
		if overlay.isProbablyTheSameIDAs(other) {
			return true
		}
	}
	return t.isProbablyTheSameIDAs(other)
}

func (ti *TemplInfo) String() string {
	if ti == nil {
		return "<nil>"
	}
	return ti.PathInfo.String()
}

func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) {
	if ti.baseVariants == nil {
		return
	}

	ti.baseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
		slashCountK2 := strings.Count(k2, "/")
		distance := slashCountK1 - slashCountK2

		for d, vv := range v {
			weight := s.dh.compareDescriptors(CategoryBaseof, ti.D, d)
			weight.distance = distance
			if best.isBetter(weight, vv.Template) {
				best.updateValues(weight, k2, d, vv.Template)
			}
		}
		return false, nil
	})
}

func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
	if t.IdentifierBase() == other.IdentifierBase() {
		return true
	}

	if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
		return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
	}

	return false
}

// Implements the additional methods in tpl.CurrentTemplateInfoOps.
func (ti *TemplInfo) Base() tpl.CurrentTemplateInfoCommonOps {
	return ti.base
}

func (ti *TemplInfo) Filename() string {
	if ti.Fi == nil {
		return ""
	}
	return ti.Fi.Meta().Filename
}

type TemplWithBaseApplied struct {
	// The template that's overlaid on top of the base template.
	Overlay *TemplInfo
	// The base template.
	Base *TemplInfo
	// This is the final template that can be used to render a page.
	Template *TemplInfo
}

// TemplateQuery is used in LookupPagesLayout to find the best matching template.
type TemplateQuery struct {
	// The path to walk down to.
	Path string

	// The name to look for. Used for shortcode queries.
	Name string

	// The category to look in.
	Category Category

	// The template descriptor to match against.
	Desc TemplateDescriptor

	// Whether to even consider this candidate.
	Consider func(candidate *TemplInfo) bool
}

func (q *TemplateQuery) init() {
	if q.Desc.Kind == kinds.KindTemporary {
		q.Desc.Kind = ""
	} else if kinds.GetKindMain(q.Desc.Kind) == "" {
		q.Desc.Kind = ""
	}
	if q.Desc.Layout == "" && q.Desc.Kind != "" {
		if q.Desc.Kind == kinds.KindPage {
			q.Desc.Layout = layoutSingle
		} else {
			q.Desc.Layout = layoutList
		}
	}

	if q.Consider == nil {
		q.Consider = func(match *TemplInfo) bool {
			return true
		}
	}

	q.Name = strings.ToLower(q.Name)

	if q.Category == 0 {
		panic("category not set")
	}
}

type TemplateStore struct {
	opts       StoreOptions
	siteOpts   SiteOptions
	htmlFormat output.Format

	treeMain        *doctree.SimpleTree[map[nodeKey]*TemplInfo]
	treeShortcodes  *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
	templatesByPath *maps.Cache[string, *TemplInfo]

	dh descriptorHandler

	// The template namespace.
	tns *templateNamespace

	// Site specific state.
	// All above this is reused.
	storeSite *storeSite

	// For testing benchmarking.
	optsOrig     StoreOptions
	siteOptsOrig SiteOptions

	// caches. These need to be refreshed when the templates are refreshed.
	cacheLookupPartials *maps.Cache[string, *TemplInfo]
}

// NewFromOpts creates a new store with the same configuration as the original.
// Used for testing/benchmarking.
func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
	return NewStore(s.optsOrig, s.siteOptsOrig)
}

// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
// combination that would be used.
// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
	var result []keyTemplateInfo
	descBaseof := desc
	s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
		for _, vv := range v {
			if vv.category != CategoryBaseof {
				continue
			}

			if vv.D.isKindInLayout(desc.Layout) && s.dh.compareDescriptors(CategoryBaseof, descBaseof, vv.D).w1 > 0 {
				result = append(result, keyTemplateInfo{Key: k, Info: vv})
			}
		}
		return false, nil
	})

	return result
}

func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
	defer func() {
		ti.executionCounter.Add(1)
		if ti.base != nil {
			ti.base.executionCounter.Add(1)
		}
	}()

	templ := ti.Template

	currentTi := &tpl.CurrentTemplateInfo{
		Parent:                 tpl.Context.CurrentTemplate.Get(ctx),
		CurrentTemplateInfoOps: ti,
	}

	ctx = tpl.Context.CurrentTemplate.Set(ctx, currentTi)

	if t.opts.Metrics != nil {
		defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
	}

	execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
	if execErr != nil {
		return t.addFileContext(ti, execErr)
	}
	return nil
}

func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
	v, found := t.storeSite.execHelper.funcs[name]
	return v, found
}

func (s *TemplateStore) GetIdentity(p string) identity.Identity {
	p = paths.AddLeadingSlash(p)
	v, found := s.templatesByPath.Get(p)
	if !found {
		return nil
	}
	return v.GetIdentity()
}

func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
	v, _ := t.templatesByPath.Get(templatePath)
	return v
}

var bestPool = sync.Pool{
	New: func() any {
		return &bestMatch{}
	},
}

func (s *TemplateStore) getBest() *bestMatch {
	v := bestPool.Get()
	b := v.(*bestMatch)
	b.defaultOutputformat = s.opts.DefaultOutputFormat
	return b
}

func (s *TemplateStore) putBest(b *bestMatch) {
	b.reset()
	bestPool.Put(b)
}

func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
	q.init()
	key := s.key(q.Path)

	slashCountKey := strings.Count(key, "/")
	best1 := s.getBest()
	defer s.putBest(best1)
	s.findBestMatchWalkPath(q, key, slashCountKey, best1)
	if best1.w.w1 <= 0 {
		return nil
	}
	m := best1.templ
	if m.noBaseOf {
		return m
	}
	best1.reset()
	m.findBestMatchBaseof(s, key, slashCountKey, best1)
	if best1.w.w1 <= 0 {
		return nil
	}
	return best1.templ
}

func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
	ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) {
		d := s.templateDescriptorFromPath(pth)
		desc := d.Desc
		if desc.Layout != "" {
			panic("shortcode template descriptor must not have a layout")
		}
		best := s.getBest()
		defer s.putBest(best)
		s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best)
		return best.templ, nil
	})

	return ti
}

func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
	q.init()
	k1 := s.key(q.Path)

	slashCountK1 := strings.Count(k1, "/")

	best := s.getBest()
	defer s.putBest(best)

	s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
		slashCountK2 := strings.Count(k2, "/")
		distance := slashCountK1 - slashCountK2

		v, found := m[q.Name]
		if !found {
			return false, nil
		}

		for k, vv := range v {
			if !q.Consider(vv) {
				continue
			}

			weight := s.dh.compareDescriptors(q.Category, q.Desc, k)
			weight.distance = distance
			if best.isBetter(weight, vv) {
				best.updateValues(weight, k2, k, vv)
			}
		}

		return false, nil
	})

	// Any match will do.
	return best.templ
}

// PrintDebug is for testing/debugging only.
func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
	if w == nil {
		w = os.Stdout
	}

	printOne := func(key string, vv *TemplInfo) {
		level := strings.Count(key, "/")
		if category != vv.category {
			return
		}
		s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ")
		ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s)
		fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
	}
	s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
		for _, vv := range v {
			printOne(key, vv)
		}
		return false, nil
	})
	s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
		for _, vv := range v {
			for _, vv2 := range vv {
				printOne(key, vv2)
			}
		}
		return false, nil
	})
}

func (s *TemplateStore) clearCaches() {
	s.cacheLookupPartials.Reset()
}

// RefreshFiles refreshes this store for the files matching the given predicate.
func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
	s.clearCaches()

	if err := s.tns.createPrototypesParse(); err != nil {
		return err
	}
	if err := s.insertTemplates(include, true); err != nil {
		return err
	}
	if err := s.parseTemplates(); err != nil {
		return err
	}
	if err := s.extractInlinePartials(); err != nil {
		return err
	}
	if err := s.transformTemplates(); err != nil {
		return err
	}
	if err := s.tns.createPrototypes(false); err != nil {
		return err
	}
	if err := s.prepareTemplates(); err != nil {
		return err
	}
	return nil
}

func (s *TemplateStore) HasTemplate(templatePath string) bool {
	templatePath = paths.AddLeadingSlash(templatePath)
	return s.templatesByPath.Contains(templatePath)
}

func (t *TemplateStore) TextLookup(name string) *TemplInfo {
	templ := t.tns.standaloneText.Lookup(name)
	if templ == nil {
		return nil
	}
	return &TemplInfo{
		Template: templ,
	}
}

func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
	templ, err := t.tns.standaloneText.New(name).Parse(tpl)
	if err != nil {
		return nil, err
	}
	return &TemplInfo{
		Template: templ,
	}, nil
}

func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
	var unused []*TemplInfo

	for vv := range t.templates() {
		if vv.subCategory != SubCategoryMain {
			// Skip inline partials and internal templates.
			continue
		}
		if vv.noBaseOf {
			if vv.executionCounter.Load() == 0 {
				unused = append(unused, vv)
			}
		} else {
			for vvv := range vv.BaseVariantsSeq() {
				if vvv.Template.executionCounter.Load() == 0 {
					unused = append(unused, vvv.Template)
				}
			}
		}
	}

	sort.Sort(byPath(unused))
	return unused
}

// WithSiteOpts creates a new store with the given site options.
// This is used to create per site template store, all sharing the same templates,
// but with a different template function execution context.
func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
	s.siteOpts = opts
	s.storeSite = configureSiteStorage(opts, s.opts.Watching)
	return &s
}

func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
	key = strings.ToLower(key)

	v := s.treeMain.Get(key)
	if v == nil {
		return
	}

	for k, vv := range v {
		if vv.category != category {
			continue
		}

		if consider != nil && !consider(vv) {
			continue
		}

		weight := s.dh.compareDescriptors(category, desc, k.d)
		if best.isBetter(weight, vv) {
			best.updateValues(weight, key, k.d, vv)
		}
	}
}

func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
	s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
		slashCountK2 := strings.Count(k2, "/")
		distance := slashCountK1 - slashCountK2

		for k, vv := range v {
			if vv.category != q.Category {
				continue
			}

			if !q.Consider(vv) {
				continue
			}

			weight := s.dh.compareDescriptors(q.Category, q.Desc, k.d)

			weight.distance = distance
			isBetter := best.isBetter(weight, vv)

			if isBetter {
				best.updateValues(weight, k2, k.d, vv)
			}
		}

		return false, nil
	})
}

func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
	if _, found := t.templatesByPath.Get(name); found {
		return nil
	}

	var templ tpl.Template

	if owner.D.IsPlainText {
		prototype := t.tns.parseText
		tt, err := prototype.New(name).Parse("")
		if err != nil {
			return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
		}
		tt.Tree.Root = n
		templ = tt
	} else {
		prototype := t.tns.parseHTML
		tt, err := prototype.New(name).Parse("")
		if err != nil {
			return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
		}
		tt.Tree.Root = n
		templ = tt
	}

	t.templatesByPath.Set(name, &TemplInfo{
		Fi:       owner.Fi,
		PathInfo: owner.PathInfo,
		D:        owner.D,
		Template: templ,
	})

	return nil
}

func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
	if ti.Fi == nil {
		return inerr
	}

	identifiers := s.extractIdentifiers(inerr.Error())

	checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
		lineMatcher := func(m herrors.LineMatcher) int {
			if m.Position.LineNumber != m.LineNumber {
				return -1
			}

			for _, id := range identifiers {
				if strings.Contains(m.Line, id) {
					// We found the line, but return a 0 to signal to
					// use the column from the error message.
					return 0
				}
			}
			return -1
		}

		f, err := fi.Meta().Open()
		if err != nil {
			return inErr, false
		}
		defer f.Close()

		fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
		fe.UpdateContent(f, lineMatcher)

		if !fe.ErrorContext().Position.IsValid() {
			return inErr, false
		}
		return fe, true
	}

	inerr = fmt.Errorf("execute of template failed: %w", inerr)

	if err, ok := checkFilename(ti.Fi, inerr); ok {
		return err
	}

	if ti.base != nil {
		if err, ok := checkFilename(ti.base.Fi, inerr); ok {
			return err
		}
	}

	return inerr
}

func (s *TemplateStore) extractIdentifiers(line string) []string {
	m := identifiersRe.FindAllStringSubmatch(line, -1)
	identifiers := make([]string, len(m))
	for i := range m {
		identifiers[i] = m[i][1]
	}
	return identifiers
}

func (s *TemplateStore) extractInlinePartials() error {
	isPartialName := func(s string) bool {
		return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
	}

	p := s.tns
	// We may find both inline and external partials in the current template namespaces,
	// so only add the ones we have not seen before.
	addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
		for _, templ := range templs {
			if templ.Name() == "" || !isPartialName(templ.Name()) {
				continue
			}
			name := templ.Name()
			if !paths.HasExt(name) {
				// Assume HTML. This in line with how the lookup works.
				name = name + ".html"
			}
			if !strings.HasPrefix(name, "_") {
				name = "_" + name
			}
			pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
			ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
			if err != nil {
				return err
			}

			if ti != nil {
				ti.Template = templ
				ti.noBaseOf = true
				ti.subCategory = SubCategoryInline
				ti.D.IsPlainText = isText
			}

		}
		return nil
	}
	addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
	addIfNotSeen(true, p.templatesIn(p.parseText)...)

	for _, t := range p.baseofHtmlClones {
		if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
			return err
		}
	}
	for _, t := range p.baseofTextClones {
		if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
			return err
		}
	}
	return nil
}

func (s *TemplateStore) insertEmbedded() error {
	return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
			return nil
		}

		templb, err := embeddedTemplatesFs.ReadFile(path)
		if err != nil {
			return err
		}

		// Get the newlines on Windows in line with how we had it back when we used Go Generate
		// to write the templates to Go files.
		templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
		name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")

		insertOne := func(name, content string) error {
			pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
			var (
				ti  *TemplInfo
				err error
			)
			if pi.Section() == containerShortcodes {
				ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
				if err != nil {
					return err
				}
			} else {
				ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
				if err != nil {
					return err
				}
			}

			if ti != nil {
				// Currently none of the embedded templates need a baseof template.
				ti.noBaseOf = true
				ti.content = content
				ti.subCategory = SubCategoryEmbedded
			}

			return nil
		}

		if err := insertOne(name, templ); err != nil {
			return err
		}

		if aliases, found := embeddedTemplatesAliases[name]; found {
			for _, alias := range aliases {
				if err := insertOne(alias, templ); err != nil {
					return err
				}
			}
		}

		return nil
	})
}

func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
	s.templatesByPath.Set(p, ti)
}

func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
	k1, k2, _, d, err := s.toKeyCategoryAndDescriptor(pi)
	if err != nil {
		return nil, err
	}
	m := tree.Get(k1)
	if m == nil {
		m = make(map[string]map[TemplateDescriptor]*TemplInfo)
		tree.Insert(k1, m)
	}

	m1, found := m[k2]
	if found {
		if _, found := m1[d]; found {
			if !replace {
				return nil, nil
			}
		}
	} else {
		m1 = make(map[TemplateDescriptor]*TemplInfo)
		m[k2] = m1
	}

	ti := &TemplInfo{
		PathInfo: pi,
		Fi:       fi,
		D:        d,
		category: CategoryShortcode,
		noBaseOf: true,
	}

	m1[d] = ti

	s.setTemplateByPath(pi.Path(), ti)

	if fi != nil {
		if pi2 := fi.Meta().PathInfo; pi2 != pi {
			s.setTemplateByPath(pi2.Path(), ti)
		}
	}

	return ti, nil
}

func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
	key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi)
	// See #13577. Warn for now.
	if err != nil {
		var loc string
		if fi != nil {
			loc = fmt.Sprintf("file %q", fi.Meta().Filename)
		} else {
			loc = fmt.Sprintf("path %q", pi.Path())
		}
		s.opts.Log.Warnf("skipping template %s: %s", loc, err)
		return nil, nil
	}

	return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
}

func (s *TemplateStore) insertTemplate2(
	pi *paths.Path,
	fi hugofs.FileMetaInfo,
	key string,
	category Category,
	d TemplateDescriptor,
	replace, isLegacyMapped bool,
	tree doctree.Tree[map[nodeKey]*TemplInfo],
) (*TemplInfo, error) {
	if category == 0 {
		panic("category not set")
	}

	m := tree.Get(key)
	nk := nodeKey{c: category, d: d}

	if m == nil {
		m = make(map[nodeKey]*TemplInfo)
		tree.Insert(key, m)
	}

	if !replace {
		if v, found := m[nk]; found {
			if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) {
				// e.g. /pages/home.foo.html and  /pages/home.html where foo may be a valid language name in another site.
				return nil, nil
			}
		}
	}

	ti := &TemplInfo{
		PathInfo:       pi,
		Fi:             fi,
		D:              d,
		category:       category,
		noBaseOf:       category > CategoryLayout,
		isLegacyMapped: isLegacyMapped,
	}

	m[nk] = ti

	if !isLegacyMapped {
		s.setTemplateByPath(pi.Path(), ti)
		if fi != nil {
			if pi2 := fi.Meta().PathInfo; pi2 != pi {
				s.setTemplateByPath(pi2.Path(), ti)
			}
		}
	}

	return ti, nil
}

func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
	if include == nil {
		include = func(fi hugofs.FileMetaInfo) bool {
			return true
		}
	}

	// Set if we need to reset the base variants.
	var (
		resetBaseVariants bool
	)

	legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}

	walker := func(pth string, fi hugofs.FileMetaInfo) error {
		if fi.IsDir() {
			return nil
		}

		if isDotFile(pth) || isBackupFile(pth) {
			return nil
		}

		if !include(fi) {
			return nil
		}

		piOrig := fi.Meta().PathInfo

		// Convert any legacy value to new format.
		fromLegacyPath := func(pi *paths.Path) *paths.Path {
			p := pi.Path()
			p = strings.TrimPrefix(p, "/_default")
			if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
				// Insert an underscore so it becomes /_shortcodes or /_partials.
				p = "/_" + p[1:]
			}

			if strings.Contains(p, "-"+baseNameBaseof) {
				// Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
				// and then separated with a hyphen before the baseof keyword.
				// This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
				// /docs/list-baseof.html => /docs/baseof.list.html.
				dir, name := path.Split(p)
				hyphenIdx := strings.Index(name, "-")
				if hyphenIdx > 0 {
					id := name[:hyphenIdx]
					name = name[hyphenIdx+1+len(baseNameBaseof):]
					if !strings.HasPrefix(name, ".") {
						name = "." + name
					}
					p = path.Join(dir, baseNameBaseof+"."+id+name)
				}
			}
			if p == pi.Path() {
				return pi
			}
			return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
		}

		pi := piOrig
		var applyLegacyMapping bool
		switch pi.Section() {
		case containerPartials, containerShortcodes, containerMarkup:
			// OK.
		default:
			applyLegacyMapping = true
			pi = fromLegacyPath(pi)
		}

		if applyLegacyMapping {
			handleMapping := func(m1 legacyOrdinalMapping) {
				key := legacyTargetPathIdentifiers{
					targetPath:     m1.mapping.targetPath,
					targetCategory: m1.mapping.targetCategory,
					kind:           m1.mapping.targetDesc.Kind,
					lang:           pi.Lang(),
					ext:            pi.Ext(),
					outputFormat:   pi.OutputFormat(),
				}
				if m2, ok := legacyOrdinalMappings[key]; ok {
					if m1.ordinal < m2.m.ordinal {
						// Higher up == better match.
						legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
					}
				} else {
					legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
				}
			}

			if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
				handleMapping(m1)
			}

			if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
				handleMapping(m1)
			}

			const (
				sectionKindToken = "SECTIONKIND"
				sectionToken     = "THESECTION"
			)

			base := piOrig.PathBeforeLangAndOutputFormatAndExt()
			identifiers := pi.IdentifiersUnknown()

			// Tokens on e.g. form /SECTIONKIND/THESECTION
			insertSectionTokens := func(section string, kindOnly bool) string {
				s := base
				if !kindOnly {
					s = strings.Replace(s, section, sectionToken, 1)
				}
				s = strings.Replace(s, kinds.KindSection, sectionKindToken, 1)
				return s
			}

			for _, section := range identifiers {
				if section == baseNameBaseof {
					continue
				}
				kindOnly := isLayoutStandard(section)
				p := insertSectionTokens(section, kindOnly)
				if m1, ok := s.opts.legacyMappingSection[p]; ok {
					m1.mapping.targetPath = strings.Replace(m1.mapping.targetPath, sectionToken, section, 1)
					handleMapping(m1)
				}
			}

		}

		if replace && pi.NameNoIdentifier() == baseNameBaseof {
			// A baseof file has changed.
			resetBaseVariants = true
		}

		var ti *TemplInfo
		var err error
		if pi.Type() == paths.TypeShortcode {
			ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
			if err != nil || ti == nil {
				return err
			}
		} else {
			ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
			if err != nil || ti == nil {
				return err
			}
		}

		if err := s.tns.readTemplateInto(ti); err != nil {
			return err
		}

		return nil
	}

	if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
		if !herrors.IsNotExist(err) {
			return err
		}
		return nil
	}

	for k, v := range legacyOrdinalMappings {
		targetPath := k.targetPath
		m := v.m.mapping
		fi := v.fi
		pi := fi.Meta().PathInfo
		outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
		category := m.targetCategory
		desc := m.targetDesc
		desc.Kind = k.kind
		desc.Lang = k.lang
		desc.OutputFormat = outputFormat.Name
		desc.IsPlainText = outputFormat.IsPlainText
		desc.MediaType = mediaType.Type

		ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
		if err != nil {
			return err
		}
		if ti == nil {
			continue
		}
		ti.isLegacyMapped = true
		if err := s.tns.readTemplateInto(ti); err != nil {
			return err
		}
	}

	if resetBaseVariants {
		s.tns.baseofHtmlClones = nil
		s.tns.baseofTextClones = nil
		s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
			for _, vv := range v {
				if !vv.noBaseOf {
					vv.state = processingStateInitial
				}
			}
			return false, nil
		})
	}

	return nil
}

func (s *TemplateStore) key(dir string) string {
	dir = paths.AddLeadingSlash(dir)
	if dir == "/" {
		return ""
	}
	return paths.TrimTrailing(dir)
}

func (s *TemplateStore) parseTemplates() error {
	if err := func() error {
		// Read and parse all templates.
		for _, v := range s.treeMain.All() {
			for _, vv := range v {
				if vv.state == processingStateTransformed {
					continue
				}
				if err := s.tns.parseTemplate(vv); err != nil {
					return err
				}
			}
		}

		// Lookup and apply base templates where needed.
		for key, v := range s.treeMain.All() {
			for _, vv := range v {
				if vv.state == processingStateTransformed {
					continue
				}
				if !vv.noBaseOf {
					d := vv.D
					// Find all compatible base templates.
					baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
					if len(baseTemplates) == 0 {
						// The regular expression used to detect if a template needs a base template has some
						// rare false positives. Assume we don't need one.
						vv.noBaseOf = true
						if err := s.tns.parseTemplate(vv); err != nil {
							return err
						}
						continue
					}
					vv.baseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()

					for _, base := range baseTemplates {
						if err := s.tns.applyBaseTemplate(vv, base); err != nil {
							return err
						}
					}

				}
			}
		}

		return nil
	}(); err != nil {
		return err
	}

	// Prese shortcodes.
	for _, v := range s.treeShortcodes.All() {
		for _, vv := range v {
			for _, vvv := range vv {
				if vvv.state == processingStateTransformed {
					continue
				}
				if err := s.tns.parseTemplate(vvv); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

// prepareTemplates prepares all templates for execution.
func (s *TemplateStore) prepareTemplates() error {
	for t := range s.templates() {
		if t.category == CategoryBaseof {
			continue
		}
		if _, err := t.Prepare(); err != nil {
			return err
		}
	}
	return nil
}

type PathTemplateDescriptor struct {
	Path string
	Desc TemplateDescriptor
}

// templateDescriptorFromPath returns a template descriptor from the given path.
// This is currently used in partial lookups only.
func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor {
	var (
		mt media.Type
		of output.Format
	)

	// Common cases.
	dotCount := strings.Count(pth, ".")
	if dotCount <= 1 {
		if dotCount == 0 {
			// Asume HTML.
			of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
		} else {
			pth = strings.TrimPrefix(pth, "/")
			ext := path.Ext(pth)
			pth = strings.TrimSuffix(pth, ext)
			ext = ext[1:]
			of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
		}
	} else {
		path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
		pth = path.PathNoIdentifier()
		of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
	}

	return PathTemplateDescriptor{
		Path: pth,
		Desc: TemplateDescriptor{
			OutputFormat: of.Name,
			MediaType:    mt.Type,
			IsPlainText:  of.IsPlainText,
		},
	}
}

// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
// based on the given output format suffix and media type suffix.
// Either of the suffixes can be empty, and the function will try to find a match
// based on the other suffix. If both are empty, the function will return zero values.
func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
	var outputFormat output.Format
	var mediaType media.Type

	if ofs != "" {
		if of, found := s.opts.OutputFormats.GetByName(ofs); found {
			outputFormat = of
			mediaType = of.MediaType
		}
	}

	if mns != "" && mediaType.IsZero() {
		if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
			outputFormat = of
			mediaType = of.MediaType
		} else {
			if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
				mediaType = mt
				if outputFormat.IsZero() {
					// For e.g. index.xml we will in the default confg now have the application/rss+xml  media type.
					// Try a last time to find the output format using the SubType as the name.
					// As to template resolution, this value is currently only used to
					// decide if this is a text or HTML template.
					outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
				}
			}
		}
	}

	return outputFormat, mediaType
}

func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
	return func(yield func(*TemplInfo) bool) {
		for _, v := range s.treeMain.All() {
			for _, vv := range v {
				if !vv.noBaseOf {
					for vvv := range vv.BaseVariantsSeq() {
						if !yield(vvv.Template) {
							return
						}
					}
				} else {
					if !yield(vv) {
						return
					}
				}
			}
		}
		for _, v := range s.treeShortcodes.All() {
			for _, vv := range v {
				for _, vvv := range vv {
					if !yield(vvv) {
						return
					}
				}
			}
		}
	}
}

func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor, error) {
	k1 := p.Dir()
	k2 := ""

	outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
	nameNoIdentifier := p.NameNoIdentifier()

	var layout string
	unknownids := p.IdentifiersUnknown()
	if p.Type() == paths.TypeShortcode {
		if len(unknownids) > 1 {
			// The name is the last identifier.
			layout = unknownids[len(unknownids)-2]
		}
	} else if len(unknownids) > 0 {
		// Pick the last, closest to the base name.
		layout = unknownids[len(unknownids)-1]
	}

	d := TemplateDescriptor{
		Lang:         p.Lang(),
		OutputFormat: p.OutputFormat(),
		MediaType:    mediaType.Type,
		Kind:         p.Kind(),
		Layout:       layout,
		IsPlainText:  outputFormat.IsPlainText,
	}

	d.normalizeFromFile()

	section := p.Section()

	var category Category
	switch p.Type() {
	case paths.TypeShortcode:
		category = CategoryShortcode
	case paths.TypePartial:
		category = CategoryPartial
	case paths.TypeMarkup:
		category = CategoryMarkup
	}

	if category == 0 {
		if nameNoIdentifier == baseNameBaseof {
			category = CategoryBaseof
		} else {
			switch section {
			case "_hugo":
				category = CategoryHugo
			case "_server":
				category = CategoryServer
			default:
				category = CategoryLayout
			}
		}
	}

	if category == CategoryPartial {
		d.Layout = ""
		k1 = p.PathNoIdentifier()
	}

	if category == CategoryShortcode {
		k1 = p.PathNoIdentifier()
		parts := strings.Split(k1, "/"+containerShortcodes+"/")
		k1 = parts[0]
		if len(parts) > 1 {
			k2 = parts[1]
		}
		k1 = s.key(k1)
	}

	// Legacy layout for home page.
	if d.Layout == "index" {
		if d.Kind == "" {
			d.Kind = kinds.KindHome
		}
		d.Layout = ""
	}

	if d.Layout == d.Kind {
		d.Layout = ""
	}

	k1 = strings.TrimPrefix(k1, "/_default")
	if k1 == "/" {
		k1 = ""
	}

	if category == CategoryMarkup {
		// We store all template nodes for a given directory on the same level.
		k1 = strings.TrimSuffix(k1, "/_markup")
		parts := strings.Split(d.Layout, "-")
		if len(parts) < 2 {
			return "", "", 0, TemplateDescriptor{}, fmt.Errorf("unrecognized render hook template")
		}
		// Either 2 or 3 parts, e.g. render-codeblock-go.
		d.Variant1 = parts[1]
		if len(parts) > 2 {
			d.Variant2 = parts[2]
		}
		d.Layout = "" // This allows using page layout as part of the key for lookups.
	}

	return k1, k2, category, d, nil
}

func (s *TemplateStore) transformTemplates() error {
	lookup := func(name string, in *TemplInfo) *TemplInfo {
		if in.D.IsPlainText {
			templ := in.Template.(*texttemplate.Template).Lookup(name)
			if templ != nil {
				return &TemplInfo{
					Template: templ,
				}
			}
		} else {
			templ := in.Template.(*htmltemplate.Template).Lookup(name)
			if templ != nil {
				return &TemplInfo{
					Template: templ,
				}
			}
		}

		return nil
	}

	for vv := range s.templates() {
		if vv.state == processingStateTransformed {
			continue
		}
		vv.state = processingStateTransformed
		if vv.category == CategoryBaseof {
			continue
		}
		if !vv.noBaseOf {
			for vvv := range vv.BaseVariantsSeq() {
				tctx, err := applyTemplateTransformers(vvv.Template, lookup)
				if err != nil {
					return err
				}

				for name, node := range tctx.deferNodes {
					if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil {
						return err
					}
				}
			}
		} else {
			tctx, err := applyTemplateTransformers(vv, lookup)
			if err != nil {
				return err
			}

			for name, node := range tctx.deferNodes {
				if err := s.addDeferredTemplate(vv, name, node); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func (s *TemplateStore) init() error {
	// Before Hugo 0.146 we had a very elaborate template lookup system, especially for
	// terms and taxonomies. This is a way of preserving backwards compatibility
	// by mapping old paths into the new tree.
	s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
	s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
	s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)

	// Placeholders.
	const singular = "SINGULAR"
	const plural = "PLURAL"

	replaceTokens := func(s, singularv, pluralv string) string {
		s = strings.Replace(s, singular, singularv, -1)
		s = strings.Replace(s, plural, pluralv, -1)
		return s
	}

	hasSingularOrPlural := func(s string) bool {
		return strings.Contains(s, singular) || strings.Contains(s, plural)
	}

	expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
		var result []layoutLegacyMapping

		if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
			for s, p := range s.opts.TaxonomySingularPlural {
				target := v.target
				target.targetPath = replaceTokens(target.targetPath, s, p)
				vv := replaceTokens(v.sourcePath, s, p)
				result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
			}
		} else {
			result = append(result, v)
		}
		return result
	}

	expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
		var result []layoutLegacyMapping
		result = append(result, v)
		baseofVariant := v
		baseofVariant.sourcePath += "-" + baseNameBaseof
		baseofVariant.target.targetCategory = CategoryBaseof
		result = append(result, baseofVariant)
		return result
	}

	var terms []layoutLegacyMapping
	for _, v := range legacyTermMappings {
		terms = append(terms, expand(v)...)
	}
	var taxonomies []layoutLegacyMapping
	for _, v := range legacyTaxonomyMappings {
		taxonomies = append(taxonomies, expand(v)...)
	}
	var sections []layoutLegacyMapping
	for _, v := range legacySectionMappings {
		sections = append(sections, expandSections(v)...)
	}

	for i, m := range terms {
		s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
	}
	for i, m := range taxonomies {
		s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
	}
	for i, m := range sections {
		s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
	}

	return nil
}

type TemplateStoreProvider interface {
	GetTemplateStore() *TemplateStore
}

type TextTemplatHandler interface {
	ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
	TextLookup(name string) *TemplInfo
	TextParse(name, tpl string) (*TemplInfo, error)
}

type bestMatch struct {
	templ *TemplInfo
	desc  TemplateDescriptor
	w     weight
	key   string

	// settings.
	defaultOutputformat string
}

func (best *bestMatch) reset() {
	best.templ = nil
	best.w = weight{}
	best.desc = TemplateDescriptor{}
	best.key = ""
}

func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
	if best.templ == nil {
		// Anything is better than nothing.
		return true
	}
	if w.w1 <= 0 {
		if best.w.w1 <= 0 {
			return ti.PathInfo.Path() < best.templ.PathInfo.Path()
		}
		return false
	}

	if best.w.w1 > 0 {
		currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded
		if currentBestIsEmbedded {
			if ti.subCategory != SubCategoryEmbedded {
				return true
			}
		} else {
			if ti.subCategory == SubCategoryEmbedded {
				// Prefer user provided template.
				return false
			}
		}
	}

	if w.distance < best.w.distance {
		if w.w2 < best.w.w2 {
			return false
		}
		if w.w3 < best.w.w3 {
			return false
		}
	} else {
		if w.w1 < best.w.w1 {
			return false
		}
	}

	if w.isEqualWeights(best.w) {
		// Tie breakers.
		if w.distance < best.w.distance {
			return true
		}

		if ti.D.Layout != "" && best.desc.Layout != "" {
			return ti.D.Layout != layoutAll
		}

		return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path()
	}

	return true
}

func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
	best.w = w
	best.templ = vv
	best.desc = k
	best.key = key
}

type byPath []*TemplInfo

func (a byPath) Len() int { return len(a) }
func (a byPath) Less(i, j int) bool {
	return a[i].PathInfo.Path() < a[j].PathInfo.Path()
}

func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

type keyTemplateInfo struct {
	Key  string
	Info *TemplInfo
}

type nodeKey struct {
	c Category
	d TemplateDescriptor
}

type processingState int

// the parts of a template store that's set per site.
type storeSite struct {
	opts       SiteOptions
	execHelper *templateExecHelper
	executer   texttemplate.Executer
}

type weight struct {
	w1       int
	w2       int
	w3       int
	distance int
}

func (w weight) isEqualWeights(other weight) bool {
	return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
}

func isLayoutCustom(s string) bool {
	if s == "" || isLayoutStandard(s) {
		return false
	}
	return true
}

func isLayoutStandard(s string) bool {
	switch s {
	case layoutAll, layoutList, layoutSingle:
		return true
	default:
		return false
	}
}

func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
	funcsv := make(map[string]reflect.Value)

	for k, v := range opts.TemplateFuncs {
		vv := reflect.ValueOf(v)
		funcsv[k] = vv
	}

	// Duplicate Go's internal funcs here for faster lookups.
	for k, v := range htmltemplate.GoFuncs {
		if _, exists := funcsv[k]; !exists {
			vv, ok := v.(reflect.Value)
			if !ok {
				vv = reflect.ValueOf(v)
			}
			funcsv[k] = vv
		}
	}

	for k, v := range texttemplate.GoFuncs {
		if _, exists := funcsv[k]; !exists {
			funcsv[k] = v
		}
	}

	s := &storeSite{
		opts: opts,
		execHelper: &templateExecHelper{
			watching:   watching,
			funcs:      funcsv,
			site:       reflect.ValueOf(opts.Site),
			siteParams: reflect.ValueOf(opts.Site.Params()),
		},
	}

	s.executer = texttemplate.NewExecuter(s.execHelper)

	return s
}

func isBackupFile(path string) bool {
	return path[len(path)-1] == '~'
}

func isDotFile(path string) bool {
	return filepath.Base(path)[0] == '.'
}
