// Copyright 2025 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 tplimpl

import (
	"strings"

	"github.com/gohugoio/hugo/common/types"
	"github.com/gohugoio/hugo/hugolib/sitesmatrix"
	"github.com/gohugoio/hugo/resources/kinds"
)

const baseNameBaseof = "baseof"

// This is used both as a key and in lookups.
type TemplateDescriptor struct {
	// Group 1.
	Kind               string // page, home, section, taxonomy, term (and only those)
	LayoutFromTemplate string // list, single, all,mycustomlayout
	LayoutFromUser     string //  custom layout set in front matter, e.g. list, single, all, mycustomlayout

	// Group 2.
	OutputFormat string // rss, csv ...
	MediaType    string // text/html, text/plain, ...

	SitesHash uint64

	Variant1 string // contextual variant, e.g. "link" in render hooks."
	Variant2 string // contextual variant, e.g. "id" in render.

	// Misc.
	LayoutFromUserMustMatch bool // If set, we only look for the exact layout.
	IsPlainText             bool // Whether this is a plain text template.
	AlwaysAllowPlainText    bool // Whether to e.g. allow plain text templates to be rendered in HTML.
}

func (d *TemplateDescriptor) normalizeFromFile() {
	d.LayoutFromTemplate = strings.ToLower(d.LayoutFromTemplate)

	if d.LayoutFromTemplate == d.OutputFormat {
		d.LayoutFromTemplate = ""
	}

	if d.Kind == kinds.KindTemporary {
		d.Kind = ""
	}

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

type descriptorHandler struct {
	opts StoreOptions
}

// Note that this in this setup is usually a descriptor constructed from a page,
// so we want to find the best match for that page.
func (s descriptorHandler) compareDescriptors(category Category, this, other TemplateDescriptor, sitesMatrixThis, sitesMatrixOther sitesmatrix.VectorProvider) weight {
	if this.LayoutFromUserMustMatch && this.LayoutFromUser != other.LayoutFromTemplate {
		return weightNoMatch
	}

	w := this.doCompare(category, other, sitesMatrixThis, sitesMatrixOther)

	if w.w1 <= 0 {
		if sitesMatrixOther != nil {
			if sitesMatrixThis == nil || !sitesMatrixOther.HasAnyVector(sitesMatrixThis) {
				return w
			}
		}

		if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
			// See issue 13242.
			if this.OutputFormat != other.OutputFormat && this.OutputFormat == s.opts.DefaultOutputFormat {
				return w
			}

			w.w1 = 1
		}

		if category == CategoryShortcode {
			if (this.IsPlainText == other.IsPlainText || !other.IsPlainText) || this.AlwaysAllowPlainText {
				w.w1 = 1
			}
		}
	}

	return w
}

//lint:ignore ST1006 this vs other makes it easier to reason about.
func (this TemplateDescriptor) doCompare(category Category, other TemplateDescriptor, sitesMatrixThis, sitesMatrixOther sitesmatrix.VectorProvider) weight {
	w := weightNoMatch

	if !this.AlwaysAllowPlainText {
		// HTML in plain text is OK, but not the other way around.
		if other.IsPlainText && !this.IsPlainText {
			return w
		}
	}

	if other.Kind != "" && other.Kind != this.Kind {
		return w
	}

	if other.LayoutFromTemplate != "" && other.LayoutFromTemplate != layoutAll {
		if this.LayoutFromUser == "" || this.LayoutFromUser != other.LayoutFromTemplate {
			if other.LayoutFromTemplate != this.LayoutFromTemplate {
				return w
			}
		}
	}

	if sitesMatrixOther != nil {
		// sitesMatrixThis is usually a single Site.
		// But we also use this method to find all base template variants for a given template,
		// and in that case we may get multiple vectors (e.g. multiple languages).
		if sitesMatrixThis == nil || !sitesMatrixOther.HasAnyVector(sitesMatrixThis) {
			return w
		}
	}

	if other.OutputFormat != "" && other.OutputFormat != this.OutputFormat {
		if this.MediaType != other.MediaType {
			return w
		}

		// We want e.g. home page in amp output format (media type text/html) to
		// find a template even if one isn't specified for that output format,
		// when one exist for the html output format (same media type).
		skip := category != CategoryBaseof && (this.Kind == "" || (this.Kind != other.Kind && (this.LayoutFromTemplate != other.LayoutFromTemplate && other.LayoutFromTemplate != layoutAll)))
		if this.LayoutFromUser != "" {
			skip = skip && this.LayoutFromUser != other.LayoutFromTemplate
		}
		if skip {
			return w
		}

		// Continue.
	}

	if other.MediaType != this.MediaType {
		return w
	}

	// One example of variant1 and 2 is for render codeblocks:
	// variant1=codeblock, variant2=go (language).
	if other.Variant1 != "" {
		if other.Variant1 != this.Variant1 {
			return w
		}

		if other.Variant2 != "" && other.Variant2 != this.Variant2 {
			return w
		}
	}

	const (
		weightKind           = 5 // page, home, section, taxonomy, term (and only those)
		weightcustomLayout   = 6 // custom layout (mylayout, set in e.g. front matter)
		weightLayoutStandard = 4 // standard layouts (single,list)
		weightLayoutAll      = 2 // the "all" layout
		weightOutputFormat   = 4 // a configured output format (e.g. rss, html, json)
		weightMediaType      = 1 // a configured media type (e.g. text/html, text/plain)
		weightSitesMatrix    = 1 // a configured language (e.g. en, nn, fr, ...)
		weightVariant1       = 6 // currently used for render hooks, e.g. "link", "image"
		weightVariant2       = 4 // currently used for render hooks, e.g. the language "go" in code blocks.

		// We will use the values for group 2 and 3
		// if the distance up to the template is shorter than
		// the one we're comparing with.
		// E.g for a page in /posts/mypage.md with the
		// two templates /layouts/posts/single.html and /layouts/page.html,
		// the first one is the best match even if the second one
		// has a higher w1 value.
		weight2Group1 = 1 // kind, standardl layout (single,list,all)
		weight2Group2 = 2 // custom layout (mylayout)

		weight3 = 1 // for media type, output format, and dimensions (if not provided).
	)

	// Now we now know that the other descriptor is a subset of this.
	// Now calculate the weights.
	w.w1++

	if other.Kind != "" && other.Kind == this.Kind {
		w.w1 += weightKind
		w.w2 = weight2Group1
	}

	if other.LayoutFromTemplate != "" && (other.LayoutFromTemplate == this.LayoutFromTemplate) {
		w.w1 += weightLayoutStandard
		w.w2 = weight2Group1
	} else if other.LayoutFromTemplate == layoutAll {
		w.w1 += weightLayoutAll
		w.w2 = weight2Group1
	}

	// LayoutCustom is only set in this (usually from Page.Layout).
	if this.LayoutFromUser != "" && this.LayoutFromUser == other.LayoutFromTemplate {
		w.w1 += weightcustomLayout
		w.w2 = weight2Group2
	}

	if sitesMatrixOther != nil {
		// sitesMatrixThis is usually a single Site.
		if sitesMatrixThis != nil && sitesMatrixOther.HasAnyVector(sitesMatrixThis) {
			w.w1 += weightSitesMatrix
			if wp, ok := sitesMatrixOther.(types.WeightProvider); ok {
				w.wsm = wp.Weight()
			} else {
				w.wsm = weight3
			}
		}
	}

	if other.OutputFormat != "" && other.OutputFormat == this.OutputFormat {
		w.w1 += weightOutputFormat
		w.w3 += weight3
	}

	if other.MediaType != "" && other.MediaType == this.MediaType {
		w.w1 += weightMediaType
		w.w3 += weight3
	}

	if other.Variant1 != "" && other.Variant1 == this.Variant1 {
		w.w1 += weightVariant1
	}

	if other.Variant1 != "" && other.Variant2 == this.Variant2 {
		w.w1 += weightVariant2
	}

	return w
}

func (d TemplateDescriptor) IsZero() bool {
	return d == TemplateDescriptor{}
}

//lint:ignore ST1006 this vs other makes it easier to reason about.
func (this TemplateDescriptor) isKindInLayout(layout string) bool {
	if this.Kind == "" {
		return true
	}
	if this.Kind != kinds.KindPage {
		return layout != layoutSingle
	}
	return layout != layoutList
}
