// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hugolib

import (
	"fmt"
	"maps"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/bep/logg"
	"github.com/gobuffalo/flect"
	"github.com/gohugoio/hugo/hugofs"
	"github.com/gohugoio/hugo/hugofs/files"
	"github.com/gohugoio/hugo/hugolib/sitesmatrix"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/langs"
	"github.com/gohugoio/hugo/markup/converter"
	"github.com/gohugoio/hugo/resources"

	"github.com/gohugoio/hugo/source"

	"github.com/gohugoio/hugo/common/hashing"
	"github.com/gohugoio/hugo/common/hiter"
	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/common/hugio"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/helpers"

	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/resources/kinds"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/page/pagemeta"
	"github.com/gohugoio/hugo/resources/resource"
	"github.com/spf13/cast"
)

var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)

func (m *pageMetaSource) GetIdentity() identity.Identity {
	return m.pathInfo
}

func (m *pageMetaSource) Path() string {
	return m.pathInfo.Base()
}

func (m *pageMetaSource) PathInfo() *paths.Path {
	return m.pathInfo
}

func (m *pageMetaSource) forEeachContentNode(f func(v sitesmatrix.Vector, n contentNode) bool) bool {
	return f(sitesmatrix.Vector{}, m)
}

type pageMetaSource struct {
	pathInfo                      *paths.Path // Always set. This the canonical path to the Page.
	f                             *source.File
	pi                            *contentParseInfo
	contentAdapterSourceEntryHash uint64

	sitesMatrixBase     sitesmatrix.VectorIterator
	sitesMatrixBaseOnly bool

	// Set for standalone pages, e.g. robotsTXT.
	standaloneOutputFormat output.Format
	resources.AtomicStaler

	pageConfigSource *pagemeta.PageConfigEarly
	cascadeCompiled  *page.PageMatcherParamsConfigs

	resourcePath  string // Set for bundled pages; path relative to its bundle root.
	bundled       bool   // Set if this page is bundled inside another.
	noFrontMatter bool
	openSource    func() (hugio.ReadSeekCloser, error)
}

func (m *pageMetaSource) String() string {
	if m.f != nil {
		return fmt.Sprintf("pageMetaSource(%s, %s)", m.f.FileInfo().Meta().PathInfo, m.f.FileInfo().Meta().Filename)
	}
	return fmt.Sprintf("pageMetaSource(%s)", m.pathInfo)
}

func (m *pageMetaSource) nodeCategoryPage() {
	// Marker method.
}

func (m *pageMetaSource) nodeCategorySingle() {
	// Marker method.
}

func (m *pageMetaSource) nodeSourceEntryID() any {
	if m.f != nil && !m.f.IsContentAdapter() {
		return m.f.FileInfo().Meta().Filename
	}
	return m.contentAdapterSourceEntryHash
}

func (m *pageMetaSource) setCascadeFromMap(frontmatter map[string]any, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions, logger loggers.Logger) error {
	const (
		pageMetaKeyCascade = "cascade"
	)

	// Check for any cascade define on itself.
	if cv, found := frontmatter[pageMetaKeyCascade]; found {
		var err error
		cascade, err := page.DecodeCascadeConfig(cv)
		if err != nil {
			return err
		}
		if err := cascade.InitConfig(logger, defaultSitesMatrix, configuredDimensions); err != nil {
			return err
		}

		m.cascadeCompiled = cascade
	}
	return nil
}

var _ contentNodeCascadeProvider = (*pageState)(nil)

type pageMeta struct {
	// Shared between all dimensions of this page.
	*pageMetaSource

	pageConfig *pagemeta.PageConfigLate

	term     string // Set for kind == KindTerm.
	singular string // Set for kind == KindTerm and kind == KindTaxonomy.

	content *cachedContent // The source and the parsed page content.

	datesOriginal pagemeta.Dates // Original dates for rebuilds.
}

func (p *pageState) getCascade() *page.PageMatcherParamsConfigs {
	return p.m.cascadeCompiled
}

func (m *pageMetaSource) initEarly(h *HugoSites, cascades *page.PageMatcherParamsConfigs) error {
	if err := m.doInitEarly(h, cascades); err != nil {
		return m.wrapError(err, h.SourceFs)
	}
	return nil
}

func (m *pageMetaSource) doInitEarly(h *HugoSites, cascades *page.PageMatcherParamsConfigs) error {
	if err := m.initFrontMatter(h); err != nil {
		return err
	}

	if m.pageConfigSource.Kind == "" {
		// Resolve page kind.
		m.pageConfigSource.Kind = kinds.KindSection
		if m.pathInfo.Base() == "" {
			m.pageConfigSource.Kind = kinds.KindHome
		} else if m.pathInfo.IsBranchBundle() {
			// A section, taxonomy or term.
			tc := h.getFirstTaxonomyConfig(m.Path())
			if !tc.IsZero() {
				// Either a taxonomy or a term.
				if tc.pluralTreeKey == m.Path() {
					m.pageConfigSource.Kind = kinds.KindTaxonomy
				} else {
					m.pageConfigSource.Kind = kinds.KindTerm
				}
			}
		} else if m.f != nil {
			m.pageConfigSource.Kind = kinds.KindPage
		}
	}

	sitesMatrixBase := m.sitesMatrixBase
	if sitesMatrixBase == nil && m.f != nil {
		sitesMatrixBase = m.f.FileInfo().Meta().SitesMatrix
	}

	if m.sitesMatrixBaseOnly && sitesMatrixBase == nil {
		panic("sitesMatrixBaseOnly set, but no base sites matrix")
	}

	var fim *hugofs.FileMeta
	if m.f != nil {
		fim = m.f.FileInfo().Meta()
	}
	if err := m.pageConfigSource.CompileEarly(m.pathInfo, cascades, h.Conf, fim, sitesMatrixBase, m.sitesMatrixBaseOnly); err != nil {
		return err
	}

	if m.pageConfigSource.Frontmatter != nil {
		if err := m.setCascadeFromMap(m.pageConfigSource.Frontmatter, m.pageConfigSource.SitesMatrix, h.Conf.ConfiguredDimensions(), h.Log); err != nil {
			return err
		}
	}
	return nil
}

func (m *pageMetaSource) initFrontMatter(h *HugoSites) error {
	if m.pathInfo == nil {
		if err := m.initPathInfo(h); err != nil {
			return err
		}
	}

	if m.pageConfigSource == nil {
		m.pageConfigSource = &pagemeta.PageConfigEarly{}
	}

	// Read front matter.
	if err := m.parseFrontMatter(h, pageSourceIDCounter.Add(1)); err != nil {
		return err
	}
	var ext string
	if m.f != nil {
		if !m.f.IsContentAdapter() {
			ext = m.f.Ext()
		}
	}

	if err := m.pageConfigSource.SetMetaPreFromMap(ext, m.pi.frontMatter, h.Log, h.Conf); err != nil {
		return err
	}

	return nil
}

func (m *pageMetaSource) initPathInfo(h *HugoSites) error {
	pcfg := m.pageConfigSource
	if pcfg.Path != "" {
		s := pcfg.Path
		// Paths from content adapters should never have any extension.
		if pcfg.IsFromContentAdapter || !paths.HasExt(s) {
			var (
				isBranch    bool
				isBranchSet bool
				ext         string = pcfg.ContentMediaType.FirstSuffix.Suffix
			)
			if pcfg.Kind != "" {
				isBranch = kinds.IsBranch(pcfg.Kind)
				isBranchSet = true
			}

			if !pcfg.IsFromContentAdapter {
				if m.pathInfo != nil {
					if !isBranchSet {
						isBranch = m.pathInfo.IsBranchBundle()
					}
					if m.pathInfo.Ext() != "" {
						ext = m.pathInfo.Ext()
					}
				} else if m.f != nil {
					pi := m.f.FileInfo().Meta().PathInfo
					if !isBranchSet {
						isBranch = pi.IsBranchBundle()
					}
					if pi.Ext() != "" {
						ext = pi.Ext()
					}
				}
			}
			if isBranch {
				s += "/_index." + ext
			} else {
				s += "/index." + ext
			}

		}
		m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s)
	} else {
		if m.f != nil {
			m.pathInfo = m.f.FileInfo().Meta().PathInfo
		}

		if m.pathInfo == nil {
			panic(fmt.Sprintf("missing pathInfo in %v", m))
		}
	}
	return nil
}

func (m *pageMeta) initLate(s *Site) error {
	var tc viewName

	if m.pageConfigSource.Kind == kinds.KindTerm || m.pageConfigSource.Kind == kinds.KindTaxonomy {
		if tc.IsZero() {
			tc = s.pageMap.cfg.getTaxonomyConfig(m.Path())
		}
		if tc.IsZero() {
			return fmt.Errorf("no taxonomy configuration found for %q", m.Path())
		}
		m.singular = tc.singular

		if m.pageConfigSource.Kind == kinds.KindTerm {
			m.term = paths.TrimLeading(strings.TrimPrefix(m.pathInfo.Unnormalized().Base(), "/"+tc.plural))
		}
	}

	return nil
}

// bookmark1
func (h *HugoSites) newPageMetaSourceFromFile(fi hugofs.FileMetaInfo) (*pageMetaSource, error) {
	p, err := func() (*pageMetaSource, error) {
		meta := fi.Meta()
		openSource := func() (hugio.ReadSeekCloser, error) {
			r, err := meta.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
			}
			return r, nil
		}

		p := &pageMetaSource{
			f:          source.NewFileInfo(fi),
			pathInfo:   fi.Meta().PathInfo,
			openSource: openSource,
		}

		return p, nil
	}()
	if err != nil {
		return nil, hugofs.AddFileInfoToError(err, fi, h.SourceFs)
	}

	return p, err
}

func (h *HugoSites) newPageMetaSourceForContentAdapter(fi hugofs.FileMetaInfo, sitesMatrixBase sitesmatrix.VectorIterator, pc *pagemeta.PageConfigEarly) (*pageMetaSource, error) {
	p := &pageMetaSource{
		f:                             source.NewFileInfo(fi),
		noFrontMatter:                 true,
		pageConfigSource:              pc,
		contentAdapterSourceEntryHash: pc.SourceEntryHash,
		sitesMatrixBase:               sitesMatrixBase,
		openSource:                    pc.Content.ValueAsOpenReadSeekCloser(),
	}

	return p, p.initPathInfo(h)
}

func (s *Site) newPageFromPageMetasource(ms *pageMetaSource, cascades *page.PageMatcherParamsConfigs) (*pageState, error) {
	m, err := s.newPageMetaFromPageMetasource(ms)
	if err != nil {
		return nil, err
	}
	p, err := s.newPageFromPageMeta(m, cascades)
	if err != nil {
		return nil, err
	}

	return p, nil
}

func (s *Site) newPageMetaFromPageMetasource(ms *pageMetaSource) (*pageMeta, error) {
	m := &pageMeta{
		pageMetaSource: ms,
		pageConfig: &pagemeta.PageConfigLate{
			Params: make(hmaps.Params),
		},
	}
	return m, nil
}

// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) prepareRebuild() {
	// Restore orignal date values so we can repeat aggregation.
	m.pageConfig.Dates = m.datesOriginal
}

func (m *pageMeta) BundleType() string {
	switch m.pathInfo.Type() {
	case paths.TypeLeaf:
		return "leaf"
	case paths.TypeBranch:
		return "branch"
	default:
		return ""
	}
}

func (m *pageMeta) Date() time.Time {
	return m.pageConfig.Dates.Date
}

func (m *pageMeta) PublishDate() time.Time {
	return m.pageConfig.Dates.PublishDate
}

func (m *pageMeta) Lastmod() time.Time {
	return m.pageConfig.Dates.Lastmod
}

func (m *pageMeta) ExpiryDate() time.Time {
	return m.pageConfig.Dates.ExpiryDate
}

func (m *pageMeta) Description() string {
	return m.pageConfig.Description
}

func (m *pageMeta) Draft() bool {
	return m.pageConfig.Draft
}

func (m *pageMeta) File() *source.File {
	return m.f
}

func (m *pageMeta) IsHome() bool {
	return m.Kind() == kinds.KindHome
}

func (m *pageMeta) Keywords() []string {
	return m.pageConfig.Keywords
}

func (m *pageMetaSource) Kind() string {
	return m.pageConfigSource.Kind
}

func (m *pageMeta) Layout() string {
	return m.pageConfig.Layout
}

func (m *pageMeta) LinkTitle() string {
	if m.pageConfig.LinkTitle != "" {
		return m.pageConfig.LinkTitle
	}

	return m.Title()
}

func (m *pageMeta) Name() string {
	if m.resourcePath != "" {
		return m.resourcePath
	}
	if m.Kind() == kinds.KindTerm {
		return m.pathInfo.Unnormalized().BaseNameNoIdentifier()
	}
	return m.Title()
}

func (m *pageMeta) IsNode() bool {
	return !m.IsPage()
}

func (m *pageMeta) IsPage() bool {
	return m.Kind() == kinds.KindPage
}

func (m *pageMeta) Params() hmaps.Params {
	return m.pageConfig.Params
}

func (m *pageMeta) Path() string {
	if m.IsHome() {
		// Base returns an empty string for the home page,
		// which is correct as the key to the page tree.
		return "/"
	}
	return m.pathInfo.Base()
}

func (m *pageMeta) PathInfo() *paths.Path {
	return m.pathInfo
}

func (m *pageMeta) IsSection() bool {
	return m.Kind() == kinds.KindSection
}

func (m *pageMeta) Section() string {
	return m.pathInfo.Section()
}

func (m *pageMeta) Sitemap() config.SitemapConfig {
	return m.pageConfig.Sitemap
}

func (m *pageMeta) Title() string {
	return m.pageConfig.Title
}

const defaultContentType = "page"

func (m *pageMeta) Type() string {
	if m.pageConfig.Type != "" {
		return m.pageConfig.Type
	}

	if sect := m.Section(); sect != "" {
		return sect
	}

	return defaultContentType
}

func (m *pageMeta) Weight() int {
	return m.pageConfig.Weight
}

func (ps *pageState) setMetaPost(cascades *page.PageMatcherParamsConfigs) error {
	if ps.m.pageConfigSource.Frontmatter != nil {
		if ps.m.pageConfigSource.IsFromContentAdapter {
			ps.m.pageConfig.ContentAdapterData = maps.Clone(ps.m.pageConfigSource.Frontmatter)
		} else {
			ps.m.pageConfig.Params = maps.Clone(ps.m.pageConfigSource.Frontmatter)
		}
	}

	if ps.m.pageConfig.Params == nil {
		ps.m.pageConfig.Params = make(hmaps.Params)
	}
	if ps.m.pageConfigSource.IsFromContentAdapter && ps.m.pageConfig.ContentAdapterData == nil {
		ps.m.pageConfig.ContentAdapterData = make(hmaps.Params)
	}

	// Cascade defined on itself has higher priority than inherited ones.
	allCascades := hiter.Concat(ps.m.cascadeCompiled.All(), cascades.All())

	for v := range allCascades {
		if !v.Target.Match(ps.Kind(), ps.Path(), ps.s.Conf.Environment(), ps.s.siteVector) {
			continue
		}

		for kk, vv := range v.Params {
			if _, found := ps.m.pageConfig.Params[kk]; !found {
				ps.m.pageConfig.Params[kk] = vv
			}
		}
		for kk, vv := range v.Fields {
			if ps.m.pageConfigSource.IsFromContentAdapter {
				if _, found := ps.m.pageConfig.ContentAdapterData[kk]; !found {
					ps.m.pageConfig.ContentAdapterData[kk] = vv
				}
			} else {
				if _, found := ps.m.pageConfig.Params[kk]; !found {
					ps.m.pageConfig.Params[kk] = vv
				}
			}
		}
	}

	if err := ps.setMetaPostParams(); err != nil {
		return err
	}

	if err := ps.m.applyDefaultValues(ps.s); err != nil {
		return err
	}

	// Store away any original values that may be changed from aggregation.
	ps.m.datesOriginal = ps.m.pageConfig.Dates

	return nil
}

func (ps *pageState) setMetaPostParams() error {
	pm := ps.m
	var mtime time.Time
	var contentBaseName string
	var isContentAdapter bool
	if ps.File() != nil {
		isContentAdapter = ps.File().IsContentAdapter()
		contentBaseName = ps.File().ContentBaseName()
		if ps.File().FileInfo() != nil {
			mtime = ps.File().FileInfo().ModTime()
		}
	}

	var gitAuthorDate time.Time
	if ps.gitInfo != nil {
		gitAuthorDate = ps.gitInfo.AuthorDate
	}

	descriptor := &pagemeta.FrontMatterDescriptor{
		PageConfigEarly: pm.pageConfigSource,
		PageConfigLate:  pm.pageConfig,
		BaseFilename:    contentBaseName,
		ModTime:         mtime,
		GitAuthorDate:   gitAuthorDate,
		Location:        langs.GetLocation(ps.s.Language()),
		PathOrTitle:     ps.pathOrTitle(),
	}

	if isContentAdapter {
		if err := pm.pageConfig.Compile(pm.pageConfigSource, ps.s.Log, ps.s.conf.OutputFormats.Config); err != nil {
			return err
		}
	}

	pcfg := pm.pageConfig

	// Handle the date separately
	// TODO(bep) we need to "do more" in this area so this can be split up and
	// more easily tested without the Page, but the coupling is strong.
	err := ps.s.frontmatterHandler.HandleDates(descriptor)
	if err != nil {
		ps.s.Log.Errorf("Failed to handle dates for page %q: %s", ps.pathOrTitle(), err)
	}

	if isContentAdapter {
		// Done.
		return nil
	}

	var sitemapSet bool

	var buildConfig any
	var isNewBuildKeyword bool
	if v, ok := pcfg.Params["_build"]; ok {
		hugo.Deprecate("The \"_build\" front matter key", "Use \"build\" instead. See https://gohugo.io/content-management/build-options.", "0.145.0")
		buildConfig = v
	} else {
		buildConfig = pcfg.Params["build"]
		isNewBuildKeyword = true
	}
	pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig)
	if err != nil {
		var msgDetail string
		if isNewBuildKeyword {
			msgDetail = `. We renamed the _build keyword to build in Hugo 0.123.0. We recommend putting user defined params in the params section, e.g.:
---
title: "My Title"
params:
  build: "My Build"
---
´

`
		}
		return fmt.Errorf("failed to decode build config in front matter: %s%s", err, msgDetail)
	}

	var draft, published, isCJKLanguage *bool
	var userParams map[string]any
	for k, v := range pcfg.Params {
		loki := strings.ToLower(k)

		if loki == "params" {
			vv, err := hmaps.ToStringMapE(v)
			if err != nil {
				return err
			}
			userParams = vv
			delete(pcfg.Params, k)
			continue
		}

		if loki == "published" { // Intentionally undocumented
			vv, err := cast.ToBoolE(v)
			if err == nil {
				published = &vv
			}
			// published may also be a date
			continue
		}

		if ps.s.frontmatterHandler.IsDateKey(loki) {
			continue
		}

		if loki == "path" || loki == "kind" || loki == "lang" {
			// See issue 12484.
			hugo.DeprecateLevelMin(loki+" in front matter", "", "v0.144.0", logg.LevelWarn)
		}

		switch loki {
		case "title":
			pcfg.Title = cast.ToString(v)
			pcfg.Params[loki] = pcfg.Title
		case "linktitle":
			pcfg.LinkTitle = cast.ToString(v)
			pcfg.Params[loki] = pcfg.LinkTitle
		case "summary":
			pcfg.Summary = cast.ToString(v)
			pcfg.Params[loki] = pcfg.Summary
		case "description":
			pcfg.Description = cast.ToString(v)
			pcfg.Params[loki] = pcfg.Description
		case "slug":
			// Don't start or end with a -
			pcfg.Slug = strings.Trim(cast.ToString(v), "-")
			pcfg.Params[loki] = pm.Slug()
		case "url":
			url := cast.ToString(v)
			if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
				return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, ps.pathOrTitle())
			}
			pcfg.URL = url
			pcfg.Params[loki] = url
		case "type":
			pcfg.Type = cast.ToString(v)
			pcfg.Params[loki] = pcfg.Type
		case "keywords":
			pcfg.Keywords = cast.ToStringSlice(v)
			pcfg.Params[loki] = pcfg.Keywords
		case "headless":
			// Legacy setting for leaf bundles.
			// This is since Hugo 0.63 handled in a more general way for all
			// pages.
			isHeadless := cast.ToBool(v)
			pcfg.Params[loki] = isHeadless
			if isHeadless {
				pm.pageConfig.Build.List = pagemeta.Never
				pm.pageConfig.Build.Render = pagemeta.Never
			}
		case "outputs":
			o := cast.ToStringSlice(v)
			// lower case names:
			for i, s := range o {
				o[i] = strings.ToLower(s)
			}
			pm.pageConfig.Outputs = o
		case "draft":
			draft = new(bool)
			*draft = cast.ToBool(v)
		case "layout":
			pcfg.Layout = cast.ToString(v)
			pcfg.Params[loki] = pcfg.Layout
		case "weight":
			pcfg.Weight = cast.ToInt(v)
			pcfg.Params[loki] = pcfg.Weight
		case "aliases":
			pcfg.Aliases = cast.ToStringSlice(v)
			for i, alias := range pcfg.Aliases {
				if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
					return fmt.Errorf("http* aliases not supported: %q", alias)
				}
				pcfg.Aliases[i] = filepath.ToSlash(alias)
			}
			pcfg.Params[loki] = pcfg.Aliases
		case "sitemap":
			pcfg.Sitemap, err = config.DecodeSitemap(ps.s.conf.Sitemap, hmaps.ToStringMap(v))
			if err != nil {
				return fmt.Errorf("failed to decode sitemap config in front matter: %s", err)
			}
			sitemapSet = true
		case "iscjklanguage":
			isCJKLanguage = new(bool)
			*isCJKLanguage = cast.ToBool(v)
		case "translationkey":
			pcfg.TranslationKey = cast.ToString(v)
			pcfg.Params[loki] = pcfg.TranslationKey
		case "resources":
			var resources []map[string]any
			handled := true

			switch vv := v.(type) {
			case []map[any]any:
				for _, vvv := range vv {
					resources = append(resources, hmaps.ToStringMap(vvv))
				}
			case []map[string]any:
				resources = append(resources, vv...)
			case []any:
				for _, vvv := range vv {
					switch vvvv := vvv.(type) {
					case map[any]any:
						resources = append(resources, hmaps.ToStringMap(vvvv))
					case map[string]any:
						resources = append(resources, vvvv)
					}
				}
			default:
				handled = false
			}

			if handled {
				pcfg.ResourcesMeta = resources
				break
			}
			fallthrough
		default:
			// If not one of the explicit values, store in Params
			switch vv := v.(type) {
			case []any:
				if len(vv) > 0 {
					allStrings := true
					for _, vvv := range vv {
						if _, ok := vvv.(string); !ok {
							allStrings = false
							break
						}
					}
					if allStrings {
						// We need tags, keywords etc. to be []string, not []interface{}.
						a := make([]string, len(vv))
						for i, u := range vv {
							a[i] = cast.ToString(u)
						}
						pcfg.Params[loki] = a
					} else {
						pcfg.Params[loki] = vv
					}
				} else {
					pcfg.Params[loki] = []string{}
				}

			default:
				pcfg.Params[loki] = vv
			}
		}
	}

	for k, v := range userParams {
		pcfg.Params[strings.ToLower(k)] = v
	}

	if !sitemapSet {
		pcfg.Sitemap = ps.s.conf.Sitemap
	}

	if draft != nil && published != nil {
		pcfg.Draft = *draft
		ps.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", ps.File().Filename())
	} else if draft != nil {
		pcfg.Draft = *draft
	} else if published != nil {
		pcfg.Draft = !*published
	}
	pcfg.Params["draft"] = pcfg.Draft

	if isCJKLanguage != nil {
		pcfg.IsCJKLanguage = *isCJKLanguage
	} else if ps.s.conf.HasCJKLanguage && ps.m.content.pi.openSource != nil {
		if cjkRe.Match(ps.m.content.mustSource()) {
			pcfg.IsCJKLanguage = true
		} else {
			pcfg.IsCJKLanguage = false
		}
	}

	pcfg.Params["iscjklanguage"] = pcfg.IsCJKLanguage

	if err := pm.pageConfigSource.Init(false); err != nil {
		return err
	}

	if err := pcfg.Init(); err != nil {
		return err
	}

	if err := pcfg.Compile(pm.pageConfigSource, ps.s.Log, ps.s.conf.OutputFormats.Config); err != nil {
		return err
	}

	return nil
}

// shouldList returns whether this page should be included in the list of pages.
// global indicates site.Pages etc.
func (m *pageMeta) shouldList(global bool) bool {
	if m.isStandalone() {
		// Never list 404, sitemap and similar.
		return false
	}

	switch m.pageConfig.Build.List {
	case pagemeta.Always:
		return true
	case pagemeta.Never:
		return false
	case pagemeta.ListLocally:
		return !global
	}
	return false
}

func (m *pageMeta) shouldListAny() bool {
	return m.shouldList(true) || m.shouldList(false)
}

func (m *pageMeta) isStandalone() bool {
	return !m.standaloneOutputFormat.IsZero()
}

func (m *pageMeta) shouldBeCheckedForMenuDefinitions() bool {
	if !m.shouldList(false) {
		return false
	}

	return m.Kind() == kinds.KindHome || m.Kind() == kinds.KindSection || m.Kind() == kinds.KindPage
}

func (m *pageMeta) noRender() bool {
	return m.pageConfig.Build.Render != pagemeta.Always
}

func (m *pageMeta) noLink() bool {
	return m.pageConfig.Build.Render == pagemeta.Never
}

func (m *pageMeta) applyDefaultValues(s *Site) error {
	if m.pageConfig.Build.IsZero() {
		m.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil)
	}

	if !s.conf.IsKindEnabled(m.Kind()) {
		(&m.pageConfig.Build).Disable()
	}

	if m.pageConfigSource.Content.Markup == "" {
		if m.File() != nil {
			// Fall back to file extension
			m.pageConfigSource.Content.Markup = s.ContentSpec.ResolveMarkup(m.File().Ext())
		}
		if m.pageConfigSource.Content.Markup == "" {
			m.pageConfigSource.Content.Markup = "markdown"
		}
	}

	if m.pageConfig.Title == "" && m.f == nil {
		switch m.Kind() {
		case kinds.KindHome:
			m.pageConfig.Title = s.Title()
		case kinds.KindSection:
			sectionName := m.pathInfo.Unnormalized().BaseNameNoIdentifier()
			if s.conf.PluralizeListTitles {
				sectionName = flect.Pluralize(sectionName)
			}
			if s.conf.CapitalizeListTitles {
				sectionName = s.conf.C.CreateTitle(sectionName)
			}
			m.pageConfig.Title = sectionName
		case kinds.KindTerm:
			if m.term != "" {
				if s.conf.CapitalizeListTitles {
					m.pageConfig.Title = s.conf.C.CreateTitle(m.term)
				} else {
					m.pageConfig.Title = m.term
				}
			} else {
				panic("term not set")
			}
		case kinds.KindTaxonomy:
			if s.conf.CapitalizeListTitles {
				m.pageConfig.Title = strings.Replace(s.conf.C.CreateTitle(m.pathInfo.Unnormalized().BaseNameNoIdentifier()), "-", " ", -1)
			} else {
				m.pageConfig.Title = strings.Replace(m.pathInfo.Unnormalized().BaseNameNoIdentifier(), "-", " ", -1)
			}
		case kinds.KindStatus404:
			m.pageConfig.Title = "404 Page not found"
		}
	}

	return nil
}

func (m *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) {
	if ps == nil {
		panic("no Page provided")
	}
	cp := ps.s.ContentSpec.Converters.Get(markup)
	if cp == nil {
		return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q, page: %s", markup, ps.getPageInfoForError())
	}

	var filename string
	var path string
	if m.f != nil {
		filename = m.f.Filename()
		path = m.f.Path()
	} else {
		path = m.Path()
	}

	doc := newPageForRenderHook(ps)

	documentLookup := func(id uint64) any {
		if id == ps.pid {
			// This prevents infinite recursion in some cases.
			return doc
		}
		if v, ok := ps.pageOutput.pco.otherOutputs.Get(id); ok {
			return v.po.p
		}
		return nil
	}

	cpp, err := cp.New(
		converter.DocumentContext{
			Document:       doc,
			DocumentLookup: documentLookup,
			DocumentID:     hashing.XxHashFromStringHexEncoded(ps.Path()),
			DocumentName:   path,
			Filename:       filename,
		},
	)
	if err != nil {
		return converter.NopConverter, err
	}

	return cpp, nil
}

// The output formats this page will be rendered to.
func (ps *pageState) outputFormats() output.Formats {
	if len(ps.m.pageConfig.ConfiguredOutputFormats) > 0 {
		return ps.m.pageConfig.ConfiguredOutputFormats
	}
	return ps.s.conf.C.KindOutputFormats[ps.Kind()]
}

func (m *pageMeta) Slug() string {
	return m.pageConfig.Slug
}

func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any {
	v := m.Params()[strings.ToLower(key)]

	if v == nil {
		return nil
	}

	switch val := v.(type) {
	case bool:
		return val
	case string:
		if stringToLower {
			return strings.ToLower(val)
		}
		return val
	case int64, int32, int16, int8, int:
		return cast.ToInt(v)
	case float64, float32:
		return cast.ToFloat64(v)
	case time.Time:
		return val
	case []string:
		if stringToLower {
			return helpers.SliceToLower(val)
		}
		return v
	default:
		return v
	}
}

func getParamToLower(m resource.ResourceParamsProvider, key string) any {
	return getParam(m, key, true)
}
