// 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 (
	"context"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/bep/logg"
	"github.com/gobuffalo/flect"
	"github.com/gohugoio/hugo/langs"
	"github.com/gohugoio/hugo/markup/converter"
	xmaps "golang.org/x/exp/maps"

	"github.com/gohugoio/hugo/related"

	"github.com/gohugoio/hugo/source"

	"github.com/gohugoio/hugo/common/constants"
	"github.com/gohugoio/hugo/common/hashing"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/maps"
	"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}`)

type pageMeta struct {
	term     string // Set for kind == KindTerm.
	singular string // Set for kind == KindTerm and kind == KindTaxonomy.

	resource.Staler
	*pageMetaParams
	pageMetaFrontMatter

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

	resourcePath string // Set for bundled pages; path relative to its bundle root.
	bundled      bool   // Set if this page is bundled inside another.

	pathInfo *paths.Path // Always set. This the canonical path to the Page.
	f        *source.File

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

	s *Site // The site this page belongs to.
}

// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
	params := xmaps.Clone[map[string]any](m.paramsOriginal)
	m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
		Params: params,
	}
	m.pageMetaFrontMatter = pageMetaFrontMatter{}
}

type pageMetaParams struct {
	setMetaPostCount          int
	setMetaPostCascadeChanged bool

	pageConfig *pagemeta.PageConfig

	// These are only set in watch mode.
	datesOriginal   pagemeta.Dates
	paramsOriginal  map[string]any                               // contains the original params as defined in the front matter.
	cascadeOriginal *maps.Ordered[page.PageMatcher, maps.Params] // contains the original cascade as defined in the front matter.
}

// From page front matter.
type pageMetaFrontMatter struct {
	configuredOutputFormats output.Formats // outputs defined in front matter.
}

func (m *pageMetaParams) init(preserveOriginal bool) {
	if preserveOriginal {
		m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
		m.cascadeOriginal = m.pageConfig.CascadeCompiled.Clone()
	}
}

func (p *pageMeta) Aliases() []string {
	return p.pageConfig.Aliases
}

func (p *pageMeta) BundleType() string {
	switch p.pathInfo.BundleType() {
	case paths.PathTypeLeaf:
		return "leaf"
	case paths.PathTypeBranch:
		return "branch"
	default:
		return ""
	}
}

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

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

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

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

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

func (p *pageMeta) Lang() string {
	return p.s.Lang()
}

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

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

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

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

func (p *pageMeta) Kind() string {
	return p.pageConfig.Kind
}

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

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

	return p.Title()
}

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

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

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

// Param is a convenience method to do lookups in Page's and Site's Params map,
// in that order.
//
// This method is also implemented on SiteInfo.
// TODO(bep) interface
func (p *pageMeta) Param(key any) (any, error) {
	return resource.Param(p, p.s.Params(), key)
}

func (p *pageMeta) Params() maps.Params {
	return p.pageConfig.Params
}

func (p *pageMeta) Path() string {
	return p.pathInfo.Base()
}

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

// RelatedKeywords implements the related.Document interface needed for fast page searches.
func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
	v, err := p.Param(cfg.Name)
	if err != nil {
		return nil, err
	}

	return cfg.ToKeywords(v)
}

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

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

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

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

const defaultContentType = "page"

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

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

	return defaultContentType
}

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

func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error {
	frontmatter := pi.frontMatter

	if frontmatter != nil {
		pcfg := p.pageConfig
		// Needed for case insensitive fetching of params values
		maps.PrepareParams(frontmatter)
		pcfg.Params = frontmatter
		// Check for any cascade define on itself.
		if cv, found := frontmatter["cascade"]; found {
			var err error
			cascade, err := page.DecodeCascade(logger, cv)
			if err != nil {
				return err
			}
			pcfg.CascadeCompiled = cascade
		}

		// Look for path, lang and kind, all of which values we need early on.
		if v, found := frontmatter["path"]; found {
			pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v))
			pcfg.Params["path"] = pcfg.Path
		}
		if v, found := frontmatter["lang"]; found {
			lang := strings.ToLower(cast.ToString(v))
			if _, ok := conf.PathParser().LanguageIndex[lang]; ok {
				pcfg.Lang = lang
				pcfg.Params["lang"] = pcfg.Lang
			}
		}
		if v, found := frontmatter["kind"]; found {
			s := cast.ToString(v)
			if s != "" {
				pcfg.Kind = kinds.GetKindMain(s)
				if pcfg.Kind == "" {
					return fmt.Errorf("unknown kind %q in front matter", s)
				}
				pcfg.Params["kind"] = pcfg.Kind
			}
		}
	} else if p.pageMetaParams.pageConfig.Params == nil {
		p.pageConfig.Params = make(maps.Params)
	}

	p.pageMetaParams.init(conf.Watching())

	return nil
}

func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, maps.Params]) error {
	ps.m.setMetaPostCount++
	var cascadeHashPre uint64
	if ps.m.setMetaPostCount > 1 {
		cascadeHashPre = hashing.HashUint64(ps.m.pageConfig.CascadeCompiled)
		ps.m.pageConfig.CascadeCompiled = ps.m.cascadeOriginal.Clone()

	}

	// Apply cascades first so they can be overridden later.
	if cascade != nil {
		if ps.m.pageConfig.CascadeCompiled != nil {
			cascade.Range(func(k page.PageMatcher, v maps.Params) bool {
				vv, found := ps.m.pageConfig.CascadeCompiled.Get(k)
				if !found {
					ps.m.pageConfig.CascadeCompiled.Set(k, v)
				} else {
					// Merge
					for ck, cv := range v {
						if _, found := vv[ck]; !found {
							vv[ck] = cv
						}
					}
				}
				return true
			})
			cascade = ps.m.pageConfig.CascadeCompiled
		} else {
			ps.m.pageConfig.CascadeCompiled = cascade
		}
	}

	if cascade == nil {
		cascade = ps.m.pageConfig.CascadeCompiled
	}

	if ps.m.setMetaPostCount > 1 {
		ps.m.setMetaPostCascadeChanged = cascadeHashPre != hashing.HashUint64(ps.m.pageConfig.CascadeCompiled)
		if !ps.m.setMetaPostCascadeChanged {

			// No changes, restore any value that may be changed by aggregation.
			ps.m.pageConfig.Dates = ps.m.datesOriginal
			return nil
		}
		ps.m.setMetaPostPrepareRebuild()

	}

	// Cascade is also applied to itself.
	cascade.Range(func(k page.PageMatcher, v maps.Params) bool {
		if !k.Matches(ps) {
			return true
		}
		for kk, vv := range v {
			if _, found := ps.m.pageConfig.Params[kk]; !found {
				ps.m.pageConfig.Params[kk] = vv
			}
		}
		return true
	})

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

	if err := ps.m.applyDefaultValues(); 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 (p *pageState) setMetaPostParams() error {
	pm := p.m
	var mtime time.Time
	var contentBaseName string
	var ext string
	var isContentAdapter bool
	if p.File() != nil {
		isContentAdapter = p.File().IsContentAdapter()
		contentBaseName = p.File().ContentBaseName()
		if p.File().FileInfo() != nil {
			mtime = p.File().FileInfo().ModTime()
		}
		if !isContentAdapter {
			ext = p.File().Ext()
		}
	}

	var gitAuthorDate time.Time
	if !p.gitInfo.IsZero() {
		gitAuthorDate = p.gitInfo.AuthorDate
	}

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

	// 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 := pm.s.frontmatterHandler.HandleDates(descriptor)
	if err != nil {
		p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
	}

	if isContentAdapter {
		// Done.
		return nil
	}

	var buildConfig any
	var isNewBuildKeyword bool
	if v, ok := pm.pageConfig.Params["_build"]; ok {
		buildConfig = v
	} else {
		buildConfig = pm.pageConfig.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 sitemapSet bool

	pcfg := pm.pageConfig
	params := pcfg.Params
	if params == nil {
		panic("params not set for " + p.Title())
	}

	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 := maps.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 pm.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)
			params[loki] = pcfg.Title
		case "linktitle":
			pcfg.LinkTitle = cast.ToString(v)
			params[loki] = pcfg.LinkTitle
		case "summary":
			pcfg.Summary = cast.ToString(v)
			params[loki] = pcfg.Summary
		case "description":
			pcfg.Description = cast.ToString(v)
			params[loki] = pcfg.Description
		case "slug":
			// Don't start or end with a -
			pcfg.Slug = strings.Trim(cast.ToString(v), "-")
			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, p.pathOrTitle())
			}
			pcfg.URL = url
			params[loki] = url
		case "type":
			pcfg.Type = cast.ToString(v)
			params[loki] = pcfg.Type
		case "keywords":
			pcfg.Keywords = cast.ToStringSlice(v)
			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)
			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)
			}
			if len(o) > 0 {
				// Output formats are explicitly set in front matter, use those.
				outFormats, err := p.s.conf.OutputFormats.Config.GetByNames(o...)
				if err != nil {
					p.s.Log.Errorf("Failed to resolve output formats: %s", err)
				} else {
					pm.configuredOutputFormats = outFormats
					params[loki] = outFormats
				}
			}
		case "draft":
			draft = new(bool)
			*draft = cast.ToBool(v)
		case "layout":
			pcfg.Layout = cast.ToString(v)
			params[loki] = pcfg.Layout
		case "markup":
			pcfg.Content.Markup = cast.ToString(v)
			params[loki] = pcfg.Content.Markup
		case "weight":
			pcfg.Weight = cast.ToInt(v)
			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)
			}
			params[loki] = pcfg.Aliases
		case "sitemap":
			pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.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)
			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, maps.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, maps.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)
						}
						params[loki] = a
					} else {
						params[loki] = vv
					}
				} else {
					params[loki] = []string{}
				}

			default:
				params[loki] = vv
			}
		}
	}

	for k, v := range userParams {
		if _, found := params[k]; found {
			p.s.Log.Warnidf(constants.WarnFrontMatterParamsOverrides, "Hugo front matter key %q is overridden in params section.", k)
		}
		params[strings.ToLower(k)] = v
	}

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

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

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

	params["iscjklanguage"] = pcfg.IsCJKLanguage

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

	if err := pcfg.Compile("", false, ext, p.s.Log, p.s.conf.MediaTypes.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 (p *pageMeta) shouldList(global bool) bool {
	if p.isStandalone() {
		// Never list 404, sitemap and similar.
		return false
	}

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

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

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

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

	return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage
}

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

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

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

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

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

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

	return nil
}

func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) {
	if ps == nil {
		panic("no Page provided")
	}
	cp := p.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 id string
	var filename string
	var path string
	if p.f != nil {
		id = p.f.UniqueID()
		filename = p.f.Filename()
		path = p.f.Path()
	} else {
		path = p.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:     id,
			DocumentName:   path,
			Filename:       filename,
		},
	)
	if err != nil {
		return converter.NopConverter, err
	}

	return cpp, nil
}

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

func (p *pageMeta) Slug() string {
	return p.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)
}

func (ps *pageState) initLazyProviders() error {
	ps.init.Add(func(ctx context.Context) (any, error) {
		pp, err := newPagePaths(ps)
		if err != nil {
			return nil, err
		}

		var outputFormatsForPage output.Formats
		var renderFormats output.Formats

		if ps.m.standaloneOutputFormat.IsZero() {
			outputFormatsForPage = ps.m.outputFormats()
			renderFormats = ps.s.h.renderFormats
		} else {
			// One of the fixed output format pages, e.g. 404.
			outputFormatsForPage = output.Formats{ps.m.standaloneOutputFormat}
			renderFormats = outputFormatsForPage
		}

		// Prepare output formats for all sites.
		// We do this even if this page does not get rendered on
		// its own. It may be referenced via one of the site collections etc.
		// it will then need an output format.
		ps.pageOutputs = make([]*pageOutput, len(renderFormats))
		created := make(map[string]*pageOutput)
		shouldRenderPage := !ps.m.noRender()

		for i, f := range renderFormats {

			if po, found := created[f.Name]; found {
				ps.pageOutputs[i] = po
				continue
			}

			render := shouldRenderPage
			if render {
				_, render = outputFormatsForPage.GetByName(f.Name)
			}

			po := newPageOutput(ps, pp, f, render)

			// Create a content provider for the first,
			// we may be able to reuse it.
			if i == 0 {
				contentProvider, err := newPageContentOutput(po)
				if err != nil {
					return nil, err
				}
				po.setContentProvider(contentProvider)
			}

			ps.pageOutputs[i] = po
			created[f.Name] = po

		}

		if err := ps.initCommonProviders(pp); err != nil {
			return nil, err
		}

		return nil, nil
	})

	return nil
}
