// Copyright 2019 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 (
	"bytes"
	"context"
	"errors"
	"fmt"
	"html/template"
	"sync"
	"sync/atomic"

	"github.com/gohugoio/hugo/common/maps"
	"github.com/gohugoio/hugo/common/text"
	"github.com/gohugoio/hugo/identity"
	"github.com/gohugoio/hugo/tpl/tplimpl"
	"github.com/spf13/cast"

	"github.com/gohugoio/hugo/markup/converter/hooks"
	gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
	"github.com/gohugoio/hugo/markup/tableofcontents"

	"github.com/gohugoio/hugo/markup/converter"

	bp "github.com/gohugoio/hugo/bufferpool"

	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/resource"
)

var (
	nopTargetPath    = targetPathsHolder{}
	nopPagePerOutput = struct {
		resource.ResourceLinksProvider
		page.ContentProvider
		page.PageRenderProvider
		page.PaginatorProvider
		page.TableOfContentsProvider
		page.AlternativeOutputFormatsProvider

		targetPather
	}{
		page.NopPage,
		page.NopPage,
		page.NopPage,
		page.NopPage,
		page.NopPage,
		page.NopPage,
		nopTargetPath,
	}
)

func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
	cp := &pageContentOutput{
		po:           po,
		renderHooks:  &renderHooks{},
		otherOutputs: maps.NewCache[uint64, *pageContentOutput](),
	}
	return cp, nil
}

type renderHooks struct {
	getRenderer hooks.GetRendererFunc
	init        sync.Once
}

// pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct {
	po *pageOutput

	// Other pages involved in rendering of this page,
	// typically included with .RenderShortcodes.
	otherOutputs *maps.Cache[uint64, *pageContentOutput]

	contentRenderedVersion uint32      // Incremented on reset.
	contentRendered        atomic.Bool // Set on content render.

	// Renders Markdown hooks.
	renderHooks *renderHooks
}

func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) {
	pco.po.p.dependencyManagerOutput.AddIdentity(idp.GetIdentity())
}

func (pco *pageContentOutput) Reset() {
	if pco == nil {
		return
	}
	pco.contentRenderedVersion++
	pco.contentRendered.Store(false)
	pco.renderHooks = &renderHooks{}
}

func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
	if len(layout) == 0 {
		return "", errors.New("no layout given")
	}
	templ, found, err := pco.po.p.resolveTemplate(layout...)
	if err != nil {
		return "", pco.po.p.wrapError(err)
	}

	if !found {
		return "", nil
	}

	// Make sure to send the *pageState and not the *pageContentOutput to the template.
	res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
	if err != nil {
		return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
	}
	return template.HTML(res), nil
}

func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
	return pco.c().Fragments(ctx)
}

func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
	return pco.c().RenderShortcodes(ctx)
}

func (pco *pageContentOutput) Markup(opts ...any) page.Markup {
	if len(opts) > 1 {
		panic("too many arguments, expected 0 or 1")
	}
	var scope string
	if len(opts) == 1 {
		scope = cast.ToString(opts[0])
	}
	return pco.po.p.m.content.getOrCreateScope(scope, pco)
}

func (pco *pageContentOutput) c() page.Markup {
	return pco.po.p.m.content.getOrCreateScope("", pco)
}

func (pco *pageContentOutput) Content(ctx context.Context) (any, error) {
	r, err := pco.c().Render(ctx)
	if err != nil {
		return nil, err
	}
	return r.Content(ctx)
}

func (pco *pageContentOutput) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
	r, err := pco.c().Render(ctx)
	if err != nil {
		return "", err
	}
	return r.ContentWithoutSummary(ctx)
}

func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
	return pco.c().(*cachedContentScope).fragmentsHTML(ctx)
}

func (pco *pageContentOutput) Len(ctx context.Context) int {
	return pco.mustRender(ctx).Len(ctx)
}

func (pco *pageContentOutput) mustRender(ctx context.Context) page.Content {
	c, err := pco.c().Render(ctx)
	if err != nil {
		pco.fail(err)
	}
	return c
}

func (pco *pageContentOutput) fail(err error) {
	pco.po.p.s.h.FatalError(pco.po.p.wrapError(err))
}

func (pco *pageContentOutput) Plain(ctx context.Context) string {
	return pco.mustRender(ctx).Plain(ctx)
}

func (pco *pageContentOutput) PlainWords(ctx context.Context) []string {
	return pco.mustRender(ctx).PlainWords(ctx)
}

func (pco *pageContentOutput) ReadingTime(ctx context.Context) int {
	return pco.mustRender(ctx).ReadingTime(ctx)
}

func (pco *pageContentOutput) WordCount(ctx context.Context) int {
	return pco.mustRender(ctx).WordCount(ctx)
}

func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int {
	return pco.mustRender(ctx).FuzzyWordCount(ctx)
}

func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML {
	summary, err := pco.mustRender(ctx).Summary(ctx)
	if err != nil {
		pco.fail(err)
	}
	return summary.Text
}

func (pco *pageContentOutput) Truncated(ctx context.Context) bool {
	summary, err := pco.mustRender(ctx).Summary(ctx)
	if err != nil {
		pco.fail(err)
	}
	return summary.Truncated
}

func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
	return pco.c().RenderString(ctx, args...)
}

func (pco *pageContentOutput) initRenderHooks() error {
	if pco == nil {
		return nil
	}

	pco.renderHooks.init.Do(func() {
		if pco.po.p.pageOutputTemplateVariationsState.Load() == 0 {
			pco.po.p.pageOutputTemplateVariationsState.Store(1)
		}

		type cacheKey struct {
			tp hooks.RendererType
			id any
			f  output.Format
		}

		renderCache := make(map[cacheKey]any)
		var renderCacheMu sync.Mutex

		resolvePosition := func(ctx any) text.Position {
			source := pco.po.p.m.content.mustSource()
			var offset int

			switch v := ctx.(type) {
			case hooks.PositionerSourceTargetProvider:
				offset = bytes.Index(source, v.PositionerSourceTarget())
			}

			pos := pco.po.p.posFromInput(source, offset)

			if pos.LineNumber > 0 {
				// Move up to the code fence delimiter.
				// This is in line with how we report on shortcodes.
				pos.LineNumber = pos.LineNumber - 1
			}

			return pos
		}

		pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
			renderCacheMu.Lock()
			defer renderCacheMu.Unlock()

			key := cacheKey{tp: tp, id: id, f: pco.po.f}
			if r, ok := renderCache[key]; ok {
				return r
			}

			// Inherit the descriptor from the page/current output format.
			// This allows for fine-grained control of the template used for
			// rendering of e.g. links.
			base, layoutDescriptor := pco.po.p.GetInternalTemplateBasePathAndDescriptor()

			switch tp {
			case hooks.LinkRendererType:
				layoutDescriptor.Variant1 = "link"
			case hooks.ImageRendererType:
				layoutDescriptor.Variant1 = "image"
			case hooks.HeadingRendererType:
				layoutDescriptor.Variant1 = "heading"
			case hooks.PassthroughRendererType:
				layoutDescriptor.Variant1 = "passthrough"
				if id != nil {
					layoutDescriptor.Variant2 = id.(string)
				}
			case hooks.BlockquoteRendererType:
				layoutDescriptor.Variant1 = "blockquote"
				if id != nil {
					layoutDescriptor.Variant2 = id.(string)
				}
			case hooks.TableRendererType:
				layoutDescriptor.Variant1 = "table"
			case hooks.CodeBlockRendererType:
				layoutDescriptor.Variant1 = "codeblock"
				if id != nil {
					layoutDescriptor.Variant2 = id.(string)
				}
			}

			renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
			var ignoreEmbedded bool

			// For multilingual single-host sites, "auto" becomes "fallback"
			// earlier in the process.
			switch layoutDescriptor.Variant1 {
			case "link":
				ignoreEmbedded = renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedNever ||
					renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto
			case "image":
				ignoreEmbedded = renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedNever ||
					renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto
			}

			candidates := pco.po.p.s.renderFormats
			var numCandidatesFound int
			consider := func(candidate *tplimpl.TemplInfo) bool {
				if layoutDescriptor.Variant1 != candidate.D.Variant1 {
					return false
				}

				if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
					return false
				}

				if ignoreEmbedded && candidate.SubCategory() == tplimpl.SubCategoryEmbedded {
					// Don't consider the embedded hook templates.
					return false
				}

				if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
					return true
				}

				if candidate.D.OutputFormat == "" {
					numCandidatesFound++
				} else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
					numCandidatesFound++
				}

				return true
			}

			getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
				q := tplimpl.TemplateQuery{
					Path:     base,
					Category: tplimpl.CategoryMarkup,
					Desc:     layoutDescriptor,
					Consider: consider,
				}

				v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
				return v, v != nil
			}

			templ, found1 := getHookTemplate()
			if found1 && templ == nil {
				panic("found1 is true, but templ is nil")
			}

			if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
				numCandidatesFound++
			}

			if numCandidatesFound > 1 {
				// More than one output format candidate found for this hook temoplate,
				// so we cannot reuse the same rendered content.
				pco.po.p.incrPageOutputTemplateVariation()
			}

			if !found1 {
				if tp == hooks.CodeBlockRendererType {
					// No user provided template for code blocks, so we use the native Go version -- which is also faster.
					r := pco.po.p.s.ContentSpec.Converters.GetHighlighter()
					renderCache[key] = r
					return r
				}
				return nil
			}

			r := hookRendererTemplate{
				templateHandler: pco.po.p.s.GetTemplateStore(),
				templ:           templ,
				resolvePosition: resolvePosition,
			}
			renderCache[key] = r
			return r
		}
	})

	return nil
}

func (pco *pageContentOutput) getContentConverter() (converter.Converter, error) {
	if err := pco.initRenderHooks(); err != nil {
		return nil, err
	}
	return pco.po.p.getContentConverter(), nil
}

func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) {
	c, err := cp.getContentConverter()
	if err != nil {
		return nil, err
	}
	return cp.renderContentWithConverter(ctx, c, content, renderTOC)
}

func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) {
	c, err := pco.getContentConverter()
	if err != nil {
		return nil, false, err
	}
	p, ok := c.(converter.ParseRenderer)
	if !ok {
		return nil, ok, nil
	}
	rctx := converter.RenderContext{
		Ctx:         ctx,
		Src:         content,
		RenderTOC:   true,
		GetRenderer: pco.renderHooks.getRenderer,
	}
	r, err := p.Parse(rctx)
	return r, ok, err
}

func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
	c, err := pco.getContentConverter()
	if err != nil {
		return nil, false, err
	}
	p, ok := c.(converter.ParseRenderer)
	if !ok {
		return nil, ok, nil
	}
	rctx := converter.RenderContext{
		Ctx:         ctx,
		Src:         content,
		RenderTOC:   true,
		GetRenderer: pco.renderHooks.getRenderer,
	}
	r, err := p.Render(rctx, doc)
	return r, ok, err
}

func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
	r, err := c.Convert(
		converter.RenderContext{
			Ctx:         ctx,
			Src:         content,
			RenderTOC:   renderTOC,
			GetRenderer: pco.renderHooks.getRenderer,
		})
	return r, err
}

// these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface {
	targetPather
	page.PaginatorProvider
	resource.ResourceLinksProvider
}

type targetPather interface {
	targetPaths() page.TargetPaths
	getRelURL() string
}

type targetPathsHolder struct {
	// relURL is usually the same as OutputFormat.RelPermalink, but can be different
	// for non-permalinkable output formats. These shares RelPermalink with the main (first) output format.
	relURL string
	paths  page.TargetPaths
	page.OutputFormat
}

func (t targetPathsHolder) getRelURL() string {
	return t.relURL
}

func (t targetPathsHolder) targetPaths() page.TargetPaths {
	return t.paths
}

func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
	b := bp.GetBuffer()
	defer bp.PutBuffer(b)
	if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {
		return "", err
	}
	return b.String(), nil
}
