// 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 provides ways to identify values in Hugo. Used for dependency tracking etc.
package identity

import (
	"fmt"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/gohugoio/hugo/common/types"
	"github.com/gohugoio/hugo/compare"
)

const (
	// Anonymous is an Identity that can be used when identity doesn't matter.
	Anonymous = StringIdentity("__anonymous")

	// GenghisKhan is an Identity everyone relates to.
	GenghisKhan = StringIdentity("__genghiskhan")
)

var NopManager = new(nopManager)

// NewIdentityManager creates a new Manager.
func NewManager(name string, opts ...ManagerOption) Manager {
	idm := &identityManager{
		Identity: Anonymous,
		name:     name,
		ids:      Identities{},
	}

	for _, o := range opts {
		o(idm)
	}

	return idm
}

// CleanString cleans s to be suitable as an identifier.
func CleanString(s string) string {
	s = strings.ToLower(s)
	s = strings.Trim(filepath.ToSlash(s), "/")
	return "/" + path.Clean(s)
}

// CleanStringIdentity cleans s to be suitable as an identifier and wraps it in a StringIdentity.
func CleanStringIdentity(s string) StringIdentity {
	return StringIdentity(CleanString(s))
}

// GetDependencyManager returns the DependencyManager from v or nil if none found.
func GetDependencyManager(v any) Manager {
	switch vv := v.(type) {
	case Manager:
		return vv
	case types.Unwrapper:
		return GetDependencyManager(vv.Unwrapv())
	case DependencyManagerProvider:
		return vv.GetDependencyManager()
	}
	return nil
}

// FirstIdentity returns the first Identity in v, Anonymous if none found
func FirstIdentity(v any) Identity {
	var result Identity = Anonymous
	WalkIdentitiesShallow(v, func(level int, id Identity) bool {
		result = id
		return result != Anonymous
	})
	return result
}

// PrintIdentityInfo is used for debugging/tests only.
func PrintIdentityInfo(v any) {
	WalkIdentitiesDeep(v, func(level int, id Identity) bool {
		var s string
		if idm, ok := id.(*identityManager); ok {
			s = " " + idm.name
		}
		fmt.Printf("%s%s (%T)%s\n", strings.Repeat("  ", level), id.IdentifierBase(), id, s)
		return false
	})
}

func Unwrap(id Identity) Identity {
	switch t := id.(type) {
	case IdentityProvider:
		return t.GetIdentity()
	default:
		return id
	}
}

// WalkIdentitiesDeep walks identities in v and applies cb to every identity found.
// Return true from cb to terminate.
// If deep is true, it will also walk nested Identities in any Manager found.
func WalkIdentitiesDeep(v any, cb func(level int, id Identity) bool) {
	seen := make(map[Identity]bool)
	walkIdentities(v, 0, true, seen, cb)
}

// WalkIdentitiesShallow will not walk into a Manager's Identities.
// See WalkIdentitiesDeep.
// cb is called for every Identity found and returns whether to terminate the walk.
func WalkIdentitiesShallow(v any, cb func(level int, id Identity) bool) {
	walkIdentitiesShallow(v, 0, cb)
}

// WithOnAddIdentity sets a callback that will be invoked when an identity is added to the manager.
func WithOnAddIdentity(f func(id Identity)) ManagerOption {
	return func(m *identityManager) {
		m.onAddIdentity = f
	}
}

// DependencyManagerProvider provides a manager for dependencies.
type DependencyManagerProvider interface {
	GetDependencyManager() Manager
}

// DependencyManagerProviderFunc is a function that implements the DependencyManagerProvider interface.
type DependencyManagerProviderFunc func() Manager

func (d DependencyManagerProviderFunc) GetDependencyManager() Manager {
	return d()
}

// DependencyManagerScopedProvider provides a manager for dependencies with a given scope.
type DependencyManagerScopedProvider interface {
	GetDependencyManagerForScope(scope int) Manager
	GetDependencyManagerForScopesAll() []Manager
}

// ForEeachIdentityProvider provides a way iterate over identities.
type ForEeachIdentityProvider interface {
	// ForEeachIdentityProvider calls cb for each Identity.
	// If cb returns true, the iteration is terminated.
	// The return value is whether the iteration was terminated.
	ForEeachIdentity(cb func(id Identity) bool) bool
}

// ForEeachIdentityProviderFunc is a function that implements the ForEeachIdentityProvider interface.
type ForEeachIdentityProviderFunc func(func(id Identity) bool) bool

func (f ForEeachIdentityProviderFunc) ForEeachIdentity(cb func(id Identity) bool) bool {
	return f(cb)
}

// ForEeachIdentityByNameProvider provides a way to look up identities by name.
type ForEeachIdentityByNameProvider interface {
	// ForEeachIdentityByName calls cb for each Identity that relates to name.
	// If cb returns true, the iteration is terminated.
	ForEeachIdentityByName(name string, cb func(id Identity) bool)
}

type FindFirstManagerIdentityProvider interface {
	Identity
	FindFirstManagerIdentity() ManagerIdentity
}

func NewFindFirstManagerIdentityProvider(m Manager, id Identity) FindFirstManagerIdentityProvider {
	return findFirstManagerIdentity{
		Identity: Anonymous,
		ManagerIdentity: ManagerIdentity{
			Manager: m, Identity: id,
		},
	}
}

type findFirstManagerIdentity struct {
	Identity
	ManagerIdentity
}

func (f findFirstManagerIdentity) FindFirstManagerIdentity() ManagerIdentity {
	return f.ManagerIdentity
}

// Identities stores identity providers.
type Identities map[Identity]bool

func (ids Identities) AsSlice() []Identity {
	s := make([]Identity, len(ids))
	i := 0
	for v := range ids {
		s[i] = v
		i++
	}
	sort.Slice(s, func(i, j int) bool {
		return s[i].IdentifierBase() < s[j].IdentifierBase()
	})

	return s
}

func (ids Identities) String() string {
	var sb strings.Builder
	i := 0
	for id := range ids {
		sb.WriteString(fmt.Sprintf("[%s]", id.IdentifierBase()))
		if i < len(ids)-1 {
			sb.WriteString(", ")
		}
		i++
	}
	return sb.String()
}

// Identity represents a thing in Hugo (a Page, a template etc.)
// Any implementation must be comparable/hashable.
type Identity interface {
	IdentifierBase() string
}

// IdentityGroupProvider can be implemented by tightly connected types.
// Current use case is Resource transformation via Hugo Pipes.
type IdentityGroupProvider interface {
	GetIdentityGroup() Identity
}

// IdentityProvider can be implemented by types that isn't itself and Identity,
// usually because they're not comparable/hashable.
type IdentityProvider interface {
	GetIdentity() Identity
}

// SignalRebuilder is an optional interface for types that can signal a rebuild.
type SignalRebuilder interface {
	SignalRebuild(ids ...Identity)
}

// IncrementByOne implements Incrementer adding 1 every time Incr is called.
type IncrementByOne struct {
	counter uint64
}

func (c *IncrementByOne) Incr() int {
	return int(atomic.AddUint64(&c.counter, uint64(1)))
}

// Incrementer increments and returns the value.
// Typically used for IDs.
type Incrementer interface {
	Incr() int
}

// IsProbablyDependentProvider is an optional interface for Identity.
type IsProbablyDependentProvider interface {
	IsProbablyDependent(other Identity) bool
}

// IsProbablyDependencyProvider is an optional interface for Identity.
type IsProbablyDependencyProvider interface {
	IsProbablyDependency(other Identity) bool
}

// Manager  is an Identity that also manages identities, typically dependencies.
type Manager interface {
	Identity
	AddIdentity(ids ...Identity)
	AddIdentityForEach(ids ...ForEeachIdentityProvider)
	GetIdentity() Identity
	Reset()
	forEeachIdentity(func(id Identity) bool) bool
}

type ManagerOption func(m *identityManager)

// StringIdentity is an Identity that wraps a string.
type StringIdentity string

func (s StringIdentity) IdentifierBase() string {
	return string(s)
}

type identityManager struct {
	Identity

	// Only used for debugging.
	name string

	// mu protects _changes_ to this manager,
	// reads currently assumes no concurrent writes.
	mu         sync.RWMutex
	ids        Identities
	forEachIds []ForEeachIdentityProvider

	// Hooks used in debugging.
	onAddIdentity func(id Identity)
}

func (im *identityManager) AddIdentity(ids ...Identity) {
	im.mu.Lock()
	defer im.mu.Unlock()

	for _, id := range ids {
		if id == nil || id == Anonymous {
			continue
		}

		if _, found := im.ids[id]; !found {
			if im.onAddIdentity != nil {
				im.onAddIdentity(id)
			}
			im.ids[id] = true
		}
	}
}

func (im *identityManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
	im.mu.Lock()
	im.forEachIds = append(im.forEachIds, ids...)
	im.mu.Unlock()
}

func (im *identityManager) ContainsIdentity(id Identity) FinderResult {
	if im.Identity != Anonymous && id == im.Identity {
		return FinderFound
	}

	f := NewFinder(FinderConfig{Exact: true})
	r := f.Contains(id, im, -1)

	return r
}

// Managers are always anonymous.
func (im *identityManager) GetIdentity() Identity {
	return im.Identity
}

func (im *identityManager) Reset() {
	im.mu.Lock()
	im.ids = Identities{}
	im.mu.Unlock()
}

func (im *identityManager) GetDependencyManagerForScope(int) Manager {
	return im
}

func (im *identityManager) GetDependencyManagerForScopesAll() []Manager {
	return []Manager{im}
}

func (im *identityManager) String() string {
	return fmt.Sprintf("IdentityManager(%s)", im.name)
}

func (im *identityManager) forEeachIdentity(fn func(id Identity) bool) bool {
	// The absence of a lock here is deliberate. This is currently only used on server reloads
	// in a single-threaded context.
	for id := range im.ids {
		if fn(id) {
			return true
		}
	}
	for _, fe := range im.forEachIds {
		if fe.ForEeachIdentity(fn) {
			return true
		}
	}
	return false
}

type nopManager int

func (m *nopManager) AddIdentity(ids ...Identity) {
}

func (m *nopManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
}

func (m *nopManager) IdentifierBase() string {
	return ""
}

func (m *nopManager) GetIdentity() Identity {
	return Anonymous
}

func (m *nopManager) Reset() {
}

func (m *nopManager) forEeachIdentity(func(id Identity) bool) bool {
	return false
}

// returns whether further walking should be terminated.
func walkIdentities(v any, level int, deep bool, seen map[Identity]bool, cb func(level int, id Identity) bool) {
	if level > 20 {
		panic("too deep")
	}
	var cbRecursive func(level int, id Identity) bool
	cbRecursive = func(level int, id Identity) bool {
		if id == nil {
			return false
		}
		if deep && seen[id] {
			return false
		}
		seen[id] = true
		if cb(level, id) {
			return true
		}

		if deep {
			if m := GetDependencyManager(id); m != nil {
				m.forEeachIdentity(func(id2 Identity) bool {
					return walkIdentitiesShallow(id2, level+1, cbRecursive)
				})
			}
		}
		return false
	}
	walkIdentitiesShallow(v, level, cbRecursive)
}

// returns whether further walking should be terminated.
// Anonymous identities are skipped.
func walkIdentitiesShallow(v any, level int, cb func(level int, id Identity) bool) bool {
	cb2 := func(level int, id Identity) bool {
		if id == Anonymous {
			return false
		}
		if id == nil {
			return false
		}
		return cb(level, id)
	}

	if id, ok := v.(Identity); ok {
		if cb2(level, id) {
			return true
		}
	}

	if ipd, ok := v.(IdentityProvider); ok {
		if cb2(level, ipd.GetIdentity()) {
			return true
		}
	}

	if ipdgp, ok := v.(IdentityGroupProvider); ok {
		if cb2(level, ipdgp.GetIdentityGroup()) {
			return true
		}
	}

	return false
}

var (
	_ Identity             = (*orIdentity)(nil)
	_ compare.ProbablyEqer = (*orIdentity)(nil)
)

func Or(a, b Identity) Identity {
	return orIdentity{a: a, b: b}
}

type orIdentity struct {
	a, b Identity
}

func (o orIdentity) IdentifierBase() string {
	return o.a.IdentifierBase()
}

func (o orIdentity) ProbablyEq(other any) bool {
	otherID, ok := other.(Identity)
	if !ok {
		return false
	}

	return probablyEq(o.a, otherID) || probablyEq(o.b, otherID)
}

func probablyEq(a, b Identity) bool {
	if a == b {
		return true
	}

	if a == Anonymous || b == Anonymous {
		return false
	}

	if a.IdentifierBase() == b.IdentifierBase() {
		return true
	}

	if a2, ok := a.(IsProbablyDependentProvider); ok {
		return a2.IsProbablyDependent(b)
	}

	return false
}
