// 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 layouts

import (
	"strings"
	"sync"
)

// These may be used as content sections with potential conflicts. Avoid that.
var reservedSections = map[string]bool{
	"shortcodes": true,
	"partials":   true,
}

// LayoutDescriptor describes how a layout should be chosen. This is
// typically built from a Page.
type LayoutDescriptor struct {
	Type    string
	Section string

	// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
	Kind string

	// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
	KindVariants string

	Lang   string
	Layout string
	// LayoutOverride indicates what we should only look for the above layout.
	LayoutOverride bool

	// From OutputFormat and MediaType.
	OutputFormatName string
	Suffix           string

	RenderingHook bool
	Baseof        bool
}

func (d LayoutDescriptor) isList() bool {
	return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" && d.Kind != "sitemap" && d.Kind != "sitemapindex"
}

// LayoutHandler calculates the layout template to use to render a given output type.
type LayoutHandler struct {
	mu    sync.RWMutex
	cache map[LayoutDescriptor][]string
}

// NewLayoutHandler creates a new LayoutHandler.
func NewLayoutHandler() *LayoutHandler {
	return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)}
}

// For returns a layout for the given LayoutDescriptor and options.
// Layouts are rendered and cached internally.
func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) {
	// We will get lots of requests for the same layouts, so avoid recalculations.
	l.mu.RLock()
	if cacheVal, found := l.cache[d]; found {
		l.mu.RUnlock()
		return cacheVal, nil
	}
	l.mu.RUnlock()

	layouts := resolvePageTemplate(d)

	layouts = uniqueStringsReuse(layouts)

	l.mu.Lock()
	l.cache[d] = layouts
	l.mu.Unlock()

	return layouts, nil
}

type layoutBuilder struct {
	layoutVariations []string
	typeVariations   []string
	d                LayoutDescriptor
	// f                Format
}

func (l *layoutBuilder) addLayoutVariations(vars ...string) {
	for _, layoutVar := range vars {
		if l.d.Baseof && layoutVar != "baseof" {
			l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
			continue
		}
		if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
			continue
		}
		l.layoutVariations = append(l.layoutVariations, layoutVar)
	}
}

func (l *layoutBuilder) addTypeVariations(vars ...string) {
	for _, typeVar := range vars {
		if !reservedSections[typeVar] {
			if l.d.RenderingHook {
				typeVar = typeVar + renderingHookRoot
			}
			l.typeVariations = append(l.typeVariations, typeVar)
		}
	}
}

func (l *layoutBuilder) addSectionType() {
	if l.d.Section != "" {
		l.addTypeVariations(l.d.Section)
	}
}

func (l *layoutBuilder) addKind() {
	l.addLayoutVariations(l.d.Kind)
	l.addTypeVariations(l.d.Kind)
}

const renderingHookRoot = "/_markup"

func resolvePageTemplate(d LayoutDescriptor) []string {
	b := &layoutBuilder{d: d}

	if !d.RenderingHook && d.Layout != "" {
		b.addLayoutVariations(d.Layout)
	}
	if d.Type != "" {
		b.addTypeVariations(d.Type)
	}

	if d.RenderingHook {
		if d.KindVariants != "" {
			// Add the more specific variants first.
			for _, variant := range strings.Split(d.KindVariants, ",") {
				b.addLayoutVariations(d.Kind + "-" + variant)
			}
		}
		b.addLayoutVariations(d.Kind)
		b.addSectionType()
	}

	switch d.Kind {
	case "page":
		b.addLayoutVariations("single")
		b.addSectionType()
	case "home":
		b.addLayoutVariations("index", "home")
		// Also look in the root
		b.addTypeVariations("")
	case "section":
		if d.Section != "" {
			b.addLayoutVariations(d.Section)
		}
		b.addSectionType()
		b.addKind()
	case "term":
		b.addKind()
		if d.Section != "" {
			b.addLayoutVariations(d.Section)
		}
		b.addLayoutVariations("taxonomy")
		b.addTypeVariations("taxonomy")
		b.addSectionType()
	case "taxonomy":
		if d.Section != "" {
			b.addLayoutVariations(d.Section + ".terms")
		}
		b.addSectionType()
		b.addLayoutVariations("terms")
		// For legacy reasons this is deliberately put last.
		b.addKind()
	case "404":
		b.addLayoutVariations("404")
		b.addTypeVariations("")
	case "robotstxt":
		b.addLayoutVariations("robots")
		b.addTypeVariations("")
	case "sitemap":
		b.addLayoutVariations("sitemap")
		b.addTypeVariations("")
	case "sitemapindex":
		b.addLayoutVariations("sitemapindex")
		b.addTypeVariations("")
	}

	isRSS := d.OutputFormatName == "rss"
	if !d.RenderingHook && !d.Baseof && isRSS {
		// The historic and common rss.xml case
		b.addLayoutVariations("")
	}

	if d.Baseof || d.Kind != "404" {
		// Most have _default in their lookup path
		b.addTypeVariations("_default")
	}

	if d.isList() {
		// Add the common list type
		b.addLayoutVariations("list")
	}

	if d.Baseof {
		b.addLayoutVariations("baseof")
	}

	layouts := b.resolveVariations()

	if !d.RenderingHook && !d.Baseof && isRSS {
		layouts = append(layouts, "_internal/_default/rss.xml")
	}

	switch d.Kind {
	case "robotstxt":
		layouts = append(layouts, "_internal/_default/robots.txt")
	case "sitemap":
		layouts = append(layouts, "_internal/_default/sitemap.xml")
	case "sitemapindex":
		layouts = append(layouts, "_internal/_default/sitemapindex.xml")
	}

	return layouts
}

func (l *layoutBuilder) resolveVariations() []string {
	var layouts []string

	var variations []string
	name := strings.ToLower(l.d.OutputFormatName)

	if l.d.Lang != "" {
		// We prefer the most specific type before language.
		variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
	} else {
		variations = append(variations, name)
	}

	variations = append(variations, "")

	for _, typeVar := range l.typeVariations {
		for _, variation := range variations {
			for _, layoutVar := range l.layoutVariations {
				if variation == "" && layoutVar == "" {
					continue
				}

				s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix)
				if s != "" {
					layouts = append(layouts, s)
				}
			}
		}
	}

	return layouts
}

// constructLayoutPath constructs a layout path given a type, layout,
// variations, and extension.  The path constructed follows the pattern of
// type/layout.variations.extension.  If any value is empty, it will be left out
// of the path construction.
//
// Path construction requires at least 2 of 3 out of layout, variations, and extension.
// If more than one of those is empty, an empty string is returned.
func constructLayoutPath(typ, layout, variations, extension string) string {
	// we already know that layout and variations are not both empty because of
	// checks in resolveVariants().
	if extension == "" && (layout == "" || variations == "") {
		return ""
	}

	// Commence valid path construction...

	var (
		p       strings.Builder
		needDot bool
	)

	if typ != "" {
		p.WriteString(typ)
		p.WriteString("/")
	}

	if layout != "" {
		p.WriteString(layout)
		needDot = true
	}

	if variations != "" {
		if needDot {
			p.WriteString(".")
		}
		p.WriteString(variations)
		needDot = true
	}

	if extension != "" {
		if needDot {
			p.WriteString(".")
		}
		p.WriteString(extension)
	}

	return p.String()
}

// Inline this here so we can use tinygo to compile a wasm binary of this package.
func uniqueStringsReuse(s []string) []string {
	result := s[:0]
	for i, val := range s {
		var seen bool

		for j := 0; j < i; j++ {
			if s[j] == val {
				seen = true
				break
			}
		}

		if !seen {
			result = append(result, val)
		}
	}
	return result
}
