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

import (
	"bytes"
	"path"
	"path/filepath"
	"strings"
	"sync"

	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/common/urls"
	"github.com/gohugoio/hugo/helpers"
	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/resources/kinds"
)

const slash = "/"

// TargetPathDescriptor describes how a file path for a given resource
// should look like on the file system. The same descriptor is then later used to
// create both the permalinks and the relative links, paginator URLs etc.
//
// The big motivating behind this is to have only one source of truth for URLs,
// and by that also get rid of most of the fragile string parsing/encoding etc.

type TargetPathDescriptor struct {
	PathSpec *helpers.PathSpec

	Type output.Format
	Kind string

	Path    *paths.Path
	Section *paths.Path

	// For regular content pages this is either
	// 1) the Slug, if set,
	// 2) the file base name (TranslationBaseName).
	BaseName string

	// PrefixFilePath contains zero or more content dimensions used as a file
	// path prefix. Dimensions are ordered role/version/language for
	// single-host and language/role/version for multihost.
	// Example (single-host): guest/v1.0.0/en
	// Example (multihost):   en/guest/v1.0.0
	PrefixFilePath string

	// PrefixLink contains zero or more content dimensions used as a link
	// prefix. Dimensions are ordered role/version/language for single-host,
	// but the language dimension is excluded for multihost.
	// Example (single-host): guest/v1.0.0/en
	// Example (multihost):   guest/v1.0.0
	PrefixLink string

	// If in multihost mode etc., every link/path needs to be prefixed, even
	// if set in URL.
	ForcePrefix bool

	// URL from front matter if set. Will override any Slug etc.
	URL string

	// Used to create paginator links.
	Addends string

	// The expanded permalink if defined for the section, ready to use.
	ExpandedPermalink string

	// Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
	UglyURLs bool
}

// TODO(bep) move this type.
type TargetPaths struct {
	// Where to store the file on disk relative to the publish dir. OS slashes.
	TargetFilename string

	// The directory to write sub-resources of the above.
	SubResourceBaseTarget string

	// The base for creating links to sub-resources of the above.
	SubResourceBaseLink string

	// The relative permalink to this resources. Unix slashes.
	Link string
}

func (p TargetPaths) RelPermalink(s *helpers.PathSpec) string {
	return s.PrependBasePath(p.Link, false)
}

func (p TargetPaths) PermalinkForOutputFormat(s *helpers.PathSpec, f output.Format) string {
	var baseURL urls.BaseURL
	var err error
	if f.Protocol != "" {
		baseURL, err = s.Cfg.BaseURL().WithProtocol(f.Protocol)
		if err != nil {
			return ""
		}
	} else {
		baseURL = s.Cfg.BaseURL()
	}
	baseURLstr := baseURL.String()
	return s.PermalinkForBaseURL(p.Link, baseURLstr)
}

func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
	// Normalize all file Windows paths to simplify what's next.
	if helpers.FilePathSeparator != "/" {
		d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
	}

	if !d.Type.Root && d.URL != "" && !strings.HasPrefix(d.URL, "/") {
		// Treat this as a context relative URL
		d.ForcePrefix = true
	}

	if d.URL != "" {
		d.URL = filepath.ToSlash(d.URL)
		if strings.Contains(d.URL, "..") {
			d.URL = path.Join("/", d.URL)
		}
	}

	if d.Type.Root && !d.ForcePrefix {
		d.PrefixFilePath = ""
		d.PrefixLink = ""
	}

	pb := getPagePathBuilder(d)
	defer putPagePathBuilder(pb)

	pb.fullSuffix = d.Type.MediaType.FirstSuffix.FullSuffix

	// The top level index files, i.e. the home page etc., needs
	// the index base even when uglyURLs is enabled.
	needsBase := true

	pb.isUgly = (d.UglyURLs || d.Type.Ugly) && !d.Type.NoUgly
	pb.baseNameSameAsType = !d.Path.IsBundle() && d.BaseName != "" && d.BaseName == d.Type.BaseName
	indexIsUglyKind := d.Kind == kinds.KindHome || d.Kind == kinds.KindSection || d.Kind == kinds.KindTaxonomy
	indexIsUglyKind = indexIsUglyKind && pb.isUgly

	if d.ExpandedPermalink == "" && pb.baseNameSameAsType {
		pb.isUgly = true
	}

	if d.Type.Path != "" {
		pb.Add(d.Type.Path)
	}

	if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
		pb.noSubResources = true
	} else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
		if d.ExpandedPermalink != "" {
			pb.Add(d.ExpandedPermalink)
		} else {
			pb.Add(d.Section.Base())
		}
		needsBase = false
	}

	if d.Kind != kinds.KindHome && d.URL != "" {
		pb.Add(paths.FieldsSlash(d.URL)...)

		if d.Addends != "" {
			pb.Add(d.Addends)
		}

		hasDot := strings.Contains(d.URL, ".")
		hasSlash := strings.HasSuffix(d.URL, "/")

		if hasSlash || !hasDot {
			pb.Add(d.Type.BaseName + pb.fullSuffix)
		} else if hasDot {
			pb.fullSuffix = paths.Ext(d.URL)
		}

		if pb.IsHtmlIndex() {
			pb.linkUpperOffset = 1
		}

		if d.ForcePrefix {

			// Prepend language prefix if not already set in URL
			if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, "/"+d.PrefixFilePath) {
				pb.prefixPath = d.PrefixFilePath
			}

			if d.PrefixLink != "" && !strings.HasPrefix(d.URL, "/"+d.PrefixLink) {
				pb.prefixLink = d.PrefixLink
			}
		}
	} else if !kinds.IsBranch(d.Kind) {
		if d.ExpandedPermalink != "" {
			pb.Add(d.ExpandedPermalink)
		} else {
			if dir := d.Path.ContainerDir(); dir != "" {
				pb.Add(dir)
			}
			if d.BaseName != "" {
				pb.Add(d.BaseName)
			} else {
				pb.Add(d.Path.BaseNameNoIdentifier())
			}
		}

		if d.Addends != "" {
			pb.Add(d.Addends)
		}

		if pb.isUgly {
			pb.ConcatLast(pb.fullSuffix)
		} else {
			pb.Add(d.Type.BaseName + pb.fullSuffix)
		}

		if pb.IsHtmlIndex() {
			pb.linkUpperOffset = 1
		}

		if d.PrefixFilePath != "" {
			pb.prefixPath = d.PrefixFilePath
		}

		if d.PrefixLink != "" {
			pb.prefixLink = d.PrefixLink
		}
	} else {
		if d.Addends != "" {
			pb.Add(d.Addends)
		}

		needsBase = needsBase && d.Addends == ""

		if needsBase || (!pb.isUgly || indexIsUglyKind) {
			pb.Add(d.Type.BaseName + pb.fullSuffix)
		} else {
			pb.ConcatLast(pb.fullSuffix)
		}

		if !indexIsUglyKind && pb.IsHtmlIndex() {
			pb.linkUpperOffset = 1
		}

		if d.PrefixFilePath != "" {
			pb.prefixPath = d.PrefixFilePath
		}

		if d.PrefixLink != "" {
			pb.prefixLink = d.PrefixLink
		}
	}

	// if page URL is explicitly set in frontmatter,
	// preserve its value without sanitization
	if d.URL == "" {
		// Note: MakePathSanitized will lower case the path if
		// disablePathToLower isn't set.
		pb.Sanitize()
	}

	link := pb.Link()

	pagePath := pb.PathFile()

	tp.TargetFilename = filepath.FromSlash(pagePath)
	if !pb.noSubResources {
		tp.SubResourceBaseTarget = pb.PathDir()
		tp.SubResourceBaseLink = pb.LinkDir()
	}

	// paths.{URL,Path}Escape rely on url.Parse which
	// will consider # a fragment identifier, so it and
	// and everything after it will be stripped from
	// `link`, so we need to escape it first.
	link = strings.ReplaceAll(link, "#", "%23")

	if d.URL != "" {
		tp.Link = paths.URLEscape(link)
	} else {
		// This is slightly faster for when we know we don't have any
		// query or scheme etc.
		tp.Link = paths.PathEscape(link)
	}
	if tp.Link == "" {
		tp.Link = "/"
	}

	return
}

// When adding state here, remember to update putPagePathBuilder.
type pagePathBuilder struct {
	els []string
	b   bytes.Buffer

	d TargetPathDescriptor

	// Builder state.
	isUgly             bool
	baseNameSameAsType bool
	noSubResources     bool
	fullSuffix         string // File suffix including any ".".
	prefixLink         string
	prefixPath         string
	linkUpperOffset    int
}

func (p *pagePathBuilder) Add(el ...string) {
	// Filter empty and slashes.
	n := 0
	for _, e := range el {
		if e != "" && e != slash {
			el[n] = e
			n++
		}
	}
	el = el[:n]

	p.els = append(p.els, el...)
}

func (p *pagePathBuilder) ConcatLast(s string) {
	if len(p.els) == 0 {
		p.Add(s)
		return
	}
	old := p.els[len(p.els)-1]
	if old == "" {
		p.els[len(p.els)-1] = s
		return
	}
	if old[len(old)-1] == '/' {
		old = old[:len(old)-1]
	}
	p.els[len(p.els)-1] = old + s
}

func (p *pagePathBuilder) IsHtmlIndex() bool {
	return p.Last() == "index.html"
}

func (p *pagePathBuilder) Last() string {
	if p.els == nil {
		return ""
	}
	return p.els[len(p.els)-1]
}

func (p *pagePathBuilder) Link() string {
	link := p.Path(p.linkUpperOffset)

	if p.baseNameSameAsType {
		link = strings.TrimSuffix(link, p.d.BaseName)
	}

	if p.prefixLink != "" {
		link = "/" + p.prefixLink + link
	}

	if p.linkUpperOffset > 0 && !strings.HasSuffix(link, "/") {
		link += "/"
	}

	return link
}

func (p *pagePathBuilder) LinkDir() string {
	if p.noSubResources {
		return ""
	}

	pathDir := p.PathDirBase()

	if p.prefixLink != "" {
		pathDir = "/" + p.prefixLink + pathDir
	}

	return pathDir
}

func (p *pagePathBuilder) Path(upperOffset int) string {
	upper := len(p.els)
	if upperOffset > 0 {
		upper -= upperOffset
	}
	p.b.Reset()

	var hadTrailingSlash bool
	for _, el := range p.els[:upper] {
		if !hadTrailingSlash && !strings.HasPrefix(el, "/") {
			p.b.WriteByte('/')
		}
		hadTrailingSlash = strings.HasSuffix(el, "/")
		p.b.WriteString(el)
	}
	return p.b.String()
}

func (p *pagePathBuilder) PathDir() string {
	dir := p.PathDirBase()
	if p.prefixPath != "" {
		dir = "/" + p.prefixPath + dir
	}
	return dir
}

func (p *pagePathBuilder) PathDirBase() string {
	if p.noSubResources {
		return ""
	}

	dir := p.Path(0)
	isIndex := strings.HasPrefix(p.Last(), p.d.Type.BaseName+".")

	if isIndex {
		dir = paths.Dir(dir)
	} else {
		dir = strings.TrimSuffix(dir, p.fullSuffix)
	}

	if dir == "/" {
		dir = ""
	}

	return dir
}

func (p *pagePathBuilder) PathFile() string {
	dir := p.Path(0)

	if p.prefixPath != "" {
		dir = "/" + p.prefixPath + dir
	}
	return dir
}

func (p *pagePathBuilder) Prepend(el ...string) {
	p.els = append(p.els[:0], append(el, p.els[0:]...)...)
}

func (p *pagePathBuilder) Sanitize() {
	for i, el := range p.els {
		p.els[i] = p.d.PathSpec.MakePathSanitized(el)
	}
}

var pagePathBuilderPool = &sync.Pool{
	New: func() any {
		return &pagePathBuilder{}
	},
}

func getPagePathBuilder(d TargetPathDescriptor) *pagePathBuilder {
	b := pagePathBuilderPool.Get().(*pagePathBuilder)
	b.d = d
	return b
}

func putPagePathBuilder(b *pagePathBuilder) {
	b.els = b.els[:0]
	b.fullSuffix = ""
	b.baseNameSameAsType = false
	b.isUgly = false
	b.noSubResources = false
	b.prefixLink = ""
	b.prefixPath = ""
	b.linkUpperOffset = 0
	pagePathBuilderPool.Put(b)
}
