// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package generic

import (
	"context"
	"fmt"
	"sort"

	"code.forgejo.org/f3/gof3/v3/f3"
	"code.forgejo.org/f3/gof3/v3/id"
	"code.forgejo.org/f3/gof3/v3/kind"
	"code.forgejo.org/f3/gof3/v3/logger"
	"code.forgejo.org/f3/gof3/v3/path"
	"code.forgejo.org/f3/gof3/v3/util"
)

type ChildrenSlice []NodeInterface

func NewChildrenSlice(len int) ChildrenSlice {
	return make([]NodeInterface, 0, len)
}

func (o ChildrenSlice) Len() int {
	return len(o)
}

func (o ChildrenSlice) Swap(i, j int) {
	o[i], o[j] = o[j], o[i]
}

func (o ChildrenSlice) Less(i, j int) bool {
	return o[i].GetID().String() < o[j].GetID().String()
}

type NodeChildren map[id.NodeID]NodeInterface

func NewNodeChildren() NodeChildren {
	return make(NodeChildren)
}

var (
	NilNode   = &Node{isNil: true}
	NilParent = NilNode
)

type Node struct {
	logger.Logger

	tree  TreeInterface
	isNil bool
	sync  bool
	kind  kind.Kind
	id    id.NodeID

	driver NodeDriverInterface
	self   NodeInterface
	parent NodeInterface

	children NodeChildren
}

func NewElementNode() path.PathElement {
	return NewNode().(path.PathElement)
}

func NewNode() NodeInterface {
	node := &Node{}
	return node.Init(node)
}

func NewNodeFromID[T any](i T) NodeInterface {
	node := NewNode()
	node.SetID(id.NewNodeID(i))
	return node
}

func (o *Node) Init(self NodeInterface) NodeInterface {
	o.SetTree(nil)
	o.SetIsNil(true)
	o.SetKind(kind.KindNil)
	o.SetID(id.NilID)
	o.SetSelf(self)
	o.SetParent(NilNode)
	o.children = NewNodeChildren()

	return self
}

func (o *Node) GetIsNil() bool      { return o == nil || o.isNil }
func (o *Node) SetIsNil(isNil bool) { o.isNil = isNil }

func (o *Node) GetIsSync() bool     { return o.sync }
func (o *Node) SetIsSync(sync bool) { o.sync = sync }

func (o *Node) GetSelf() NodeInterface     { return o.self }
func (o *Node) SetSelf(self NodeInterface) { o.self = self }

func (o *Node) GetParent() NodeInterface       { return o.parent }
func (o *Node) SetParent(parent NodeInterface) { o.parent = parent }

func (o *Node) GetKind() kind.Kind     { return o.kind }
func (o *Node) SetKind(kind kind.Kind) { o.kind = kind }

func (o *Node) GetID() id.NodeID {
	if o.id == nil {
		return id.NilID
	}
	return o.id
}
func (o *Node) SetID(id id.NodeID) { o.id = id }

func (o *Node) GetMappedID() id.NodeID {
	mappedID := o.GetDriver().GetMappedID()
	if mappedID == nil {
		mappedID = id.NilID
	}
	return mappedID
}

func (o *Node) SetMappedID(mapped id.NodeID) {
	o.GetDriver().SetMappedID(mapped)
}

func (o *Node) GetTree() TreeInterface { return o.tree }
func (o *Node) SetTree(tree TreeInterface) {
	o.tree = tree
	if tree != nil {
		o.SetLogger(tree.GetLogger())
	}
}

func (o *Node) GetDriver() NodeDriverInterface { return o.driver }
func (o *Node) SetDriver(driver NodeDriverInterface) {
	driver.SetNode(o)
	o.driver = driver
}

func (o *Node) GetNodeChildren() NodeChildren {
	return o.children
}

func (o *Node) GetChildren() ChildrenSlice {
	children := NewChildrenSlice(len(o.children))
	for _, child := range o.children {
		children = append(children, child)
	}
	sort.Sort(children)
	return children
}

func (o *Node) SetChildren(children NodeChildren) { o.children = children }

func (o *Node) String() string {
	driver := o.GetDriver().String()
	if driver != "" {
		driver = "=" + driver
	}
	return o.GetID().String() + driver
}

func (o *Node) Equals(ctx context.Context, other NodeInterface) bool {
	return o.GetKind() == other.GetKind() && o.GetID() == other.GetID()
}

func (o *Node) GetCurrentPath() path.Path {
	var p path.Path
	if o.GetIsNil() {
		p = path.NewPath()
	} else {
		p = o.GetParent().GetCurrentPath().Append(o)
	}
	return p
}

func (o *Node) ListPage(ctx context.Context, page int) ChildrenSlice {
	return o.GetDriver().ListPage(ctx, page)
}

func (o *Node) List(ctx context.Context) ChildrenSlice {
	o.Trace("%s", o.GetKind())
	children := NewNodeChildren()

	self := o.GetSelf()
	for page := 1; ; page++ {
		util.MaybeTerminate(ctx)
		childrenPage := self.ListPage(ctx, page)
		for _, child := range childrenPage {
			children[child.GetID()] = child
		}

		if len(childrenPage) < o.GetTree().GetPageSize() {
			break
		}
	}

	o.children = children
	return o.GetChildren()
}

func (o *Node) GetChild(id id.NodeID) NodeInterface {
	child, ok := o.children[id]
	if !ok {
		return NilNode
	}
	return child
}

func (o *Node) SetChild(child NodeInterface) NodeInterface {
	o.children[child.GetID()] = child
	return child
}

func (o *Node) DeleteChild(id id.NodeID) NodeInterface {
	if child, ok := o.children[id]; ok {
		delete(o.children, id)
		return child
	}
	return NilNode
}

func (o *Node) CreateChild(ctx context.Context) NodeInterface {
	tree := o.GetTree()
	child := tree.Factory(ctx, tree.GetChildrenKind(o.GetKind()))
	child.SetParent(o)
	return child
}

func (o *Node) GetIDFromName(ctx context.Context, name string) id.NodeID {
	return o.GetDriver().GetIDFromName(ctx, name)
}

func (o *Node) Get(ctx context.Context) NodeInterface {
	if o.GetDriver().Get(ctx) {
		o.SetIsSync(true)
	}
	return o
}

func (o *Node) Upsert(ctx context.Context) NodeInterface {
	if o.GetID() != id.NilID {
		o.GetDriver().Patch(ctx)
	} else {
		o.SetID(o.GetDriver().Put(ctx))
	}
	if o.GetParent() != NilNode {
		o.GetParent().SetChild(o)
	}
	return o
}

func (o *Node) Delete(ctx context.Context) NodeInterface {
	o.GetDriver().Delete(ctx)
	return o.GetParent().DeleteChild(o.GetID())
}

func (o *Node) NewFormat() f3.Interface {
	return o.GetDriver().NewFormat()
}

func (o *Node) FromFormat(f f3.Interface) NodeInterface {
	o.SetID(id.NewNodeID(f.GetID()))
	o.GetDriver().FromFormat(f)
	return o
}

func (o *Node) ToFormat() f3.Interface {
	if o == nil || o.GetDriver() == nil {
		return nil
	}
	return o.GetDriver().ToFormat()
}

func (o *Node) LookupMappedID(id id.NodeID) id.NodeID { return o.GetDriver().LookupMappedID(id) }

func (o *Node) ApplyAndGet(ctx context.Context, p path.Path, options *ApplyOptions) bool {
	applyWrapInGet := func(options *ApplyOptions) ApplyFunc {
		return func(ctx context.Context, parent, p path.Path, node NodeInterface) {
			if options.fun != nil && (p.Empty() || options.where == ApplyEachNode) {
				options.fun(ctx, parent, p, node)
			}
			if p.Empty() {
				return
			}

			childID := p.First().(NodeInterface).GetID()
			// is it a known child?
			child := node.GetChild(childID)
			// if not, maybe it is a known child referenced by name
			if child.GetIsNil() {
				childIDFromName := node.GetIDFromName(ctx, childID.String())
				if childIDFromName != id.NilID {
					childID = childIDFromName
					child = node.GetChild(childID)
					p.First().(NodeInterface).SetID(childID)
				}
			}
			// not a known child by ID or by Name: make one
			if child.GetIsNil() {
				child = node.CreateChild(ctx)
				child.SetID(childID)
				if child.Get(ctx).GetIsSync() {
					// only set the child if the driver is able to get it, otherwise
					// it means that although the ID is known the child itself cannot be
					// obtained and is assumed to be not found
					node.SetChild(child)
				}
			}
		}
	}
	return o.Apply(ctx, path.NewPath(), p, NewApplyOptions(applyWrapInGet(options)).SetWhere(ApplyEachNode))
}

func (o *Node) WalkAndGet(ctx context.Context, parent path.Path, options *WalkOptions) {
	walkWrapInGet := func(fun WalkFunc) WalkFunc {
		return func(ctx context.Context, p path.Path, node NodeInterface) {
			node.Get(ctx)
			node.List(ctx)
			if fun != nil {
				fun(ctx, p, node)
			}
		}
	}

	o.Walk(ctx, parent, NewWalkOptions(walkWrapInGet(options.fun)))
}

func (o *Node) Walk(ctx context.Context, parent path.Path, options *WalkOptions) {
	util.MaybeTerminate(ctx)
	options.fun(ctx, parent, o.GetSelf())
	parent = parent.Append(o.GetSelf().(path.PathElement))
	for _, child := range o.GetSelf().GetChildren() {
		child.Walk(ctx, parent, options)
	}
}

func (o *Node) FindAndGet(ctx context.Context, p path.Path) NodeInterface {
	var r NodeInterface
	r = NilNode
	set := func(ctx context.Context, parent, p path.Path, node NodeInterface) {
		r = node
	}
	o.ApplyAndGet(ctx, p, NewApplyOptions(set))
	return r
}

func (o *Node) MustFind(p path.Path) NodeInterface {
	found := o.Find(p)
	if found.GetIsNil() {
		panic(fmt.Errorf("%s not found", p))
	}
	return found
}

func (o *Node) Find(p path.Path) NodeInterface {
	if p.Empty() {
		return o
	}

	child := o.GetChild(p.First().(NodeInterface).GetID())
	if child.GetIsNil() {
		o.Info("'%s' not found", p.String())
		return NilNode
	}

	return child.Find(p.RemoveFirst())
}

func (o *Node) Apply(ctx context.Context, parentPath, p path.Path, options *ApplyOptions) bool {
	o.Trace("parent '%s', node '%s', path '%s'", parentPath.ReadableString(), o.String(), p.ReadableString())

	util.MaybeTerminate(ctx)

	if p.Empty() {
		options.fun(ctx, parentPath, p, o.GetSelf())
		return true
	}

	i := p.First().(NodeInterface).GetID().String()

	if i == "." {
		return o.Apply(ctx, parentPath, p.RemoveFirst(), options)
	}

	if i == ".." {
		parent, parentPath := parentPath.Pop()
		return parent.(NodeInterface).Apply(ctx, parentPath, p.RemoveFirst(), options)
	}

	if options.where == ApplyEachNode {
		options.fun(ctx, parentPath, p, o.GetSelf())
	}

	child := o.GetChild(p.First().(NodeInterface).GetID())
	if child.GetIsNil() && options.search == ApplySearchByName {
		if childID := o.GetIDFromName(ctx, p.First().(NodeInterface).GetID().String()); childID != id.NilID {
			child = o.GetChild(childID)
		}
	}
	if child.GetIsNil() {
		return false
	}
	parentPath = parentPath.Append(o.GetSelf().(path.PathElement))
	return child.Apply(ctx, parentPath, p.RemoveFirst(), options)
}

type ErrorNodeNotFound error

func NewError[T error](message string, args ...any) T {
	e := fmt.Errorf(message, args...)
	return e.(T)
}
