/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package utils

import (
	"slices"
	"strconv"
	"strings"
)

// NewOrderedNavigableMap initializates a structure of OrderedNavigableMap with a DataNode
func NewOrderedNavigableMap() *OrderedNavigableMap {
	return &OrderedNavigableMap{
		nm:       &DataNode{Type: NMMapType, Map: make(map[string]*DataNode)},
		orderIdx: NewPathItemList(),
		orderRef: make(map[string][]*PathItemElement),
	}
}

// OrderedNavigableMap is the same as DataNode but keeps the order of fields
type OrderedNavigableMap struct {
	nm       *DataNode
	orderIdx *PathItemList
	orderRef map[string][]*PathItemElement
}

// String returns the map as json string
func (onm *OrderedNavigableMap) String() string {
	return ToJSON(onm.nm)
}

// GetFirstElement returns the first element from the order
func (onm *OrderedNavigableMap) GetFirstElement() *PathItemElement {
	return onm.orderIdx.Front()
}

// Interface returns navigble map that's inside
func (onm *OrderedNavigableMap) Interface() any {
	return onm.nm
}

// Field returns the item on the given path
func (onm *OrderedNavigableMap) Field(fldPath []string) (val *DataLeaf, err error) {
	return onm.nm.Field(fldPath)
}

// Empty returns true if the NM is empty(no data)
func (onm *OrderedNavigableMap) Empty() bool {
	return onm.nm.IsEmpty()
}

func (onm *OrderedNavigableMap) removeRef(path string) {
	for idxPath, slcIdx := range onm.orderRef { //remove the references from order
		if strings.HasPrefix(idxPath, path) {
			if lp := len(path); len(idxPath) > lp &&
				idxPath[lp] != NestingSep[0] &&
				idxPath[lp] != IdxStart[0] {
				continue
			}
			for _, el := range slcIdx {
				onm.orderIdx.Remove(el)
			}
			delete(onm.orderRef, idxPath)
		}
	}
}

// Remove removes the item for the given path and updates the order
func (onm *OrderedNavigableMap) Remove(fullPath *FullPath) (err error) {
	if fullPath.Path == EmptyString {
		return ErrWrongPath
	}
	// fullPath.PathSlice = slices.Clone(fullPath.PathSlice) // clone the items to not modify the templates
	if err = onm.nm.Remove(fullPath.PathSlice); err != nil { // remove them from DataNode
		return
	}
	onm.removeRef(fullPath.Path)
	return
}

// Set sets the value at the given path
// this used with full path and the processed path to not calculate them for every set
// used in tests
func (onm *OrderedNavigableMap) Set(fullPath *FullPath, val any) (err error) {
	if fullPath == nil || len(fullPath.PathSlice) == 0 {
		return ErrWrongPath
	}
	var addedNew bool // determine if the node was created or only overwriten
	if addedNew, err = onm.nm.Set(fullPath.PathSlice, val); err != nil {
		return
	}

	// update the  order reference
	path := stripIdxFromLastPathElm(fullPath.Path)
	_, hasRef := onm.orderRef[path]
	if addedNew || !hasRef { // the element is new so append to order
		onm.orderRef[path] = append(onm.orderRef[path], onm.orderIdx.PushBack(fullPath.PathSlice))
	} else { // move element in the back of order list
		onm.orderIdx.MoveToBack(onm.orderRef[path][len(onm.orderRef[path])-1])
	}
	return
}

// SetAsSlice sets the slice of nodes at the given path
// this used with full path and the processed path to not calculate them for every set
// this is used by agent request to overwrite the slice at path
func (onm *OrderedNavigableMap) SetAsSlice(fullPath *FullPath, vals []*DataNode) (err error) {
	if fullPath == nil || len(fullPath.PathSlice) == 0 {
		return ErrWrongPath
	}
	var addedNew bool
	if addedNew, err = onm.nm.Set(fullPath.PathSlice, vals); err != nil {
		return
	}

	pathItmsSet := make([][]string, len(vals)) // prepare the path for order update
	for i := range vals {
		pathItmsSet[i] = append(slices.Clone(fullPath.PathSlice), strconv.Itoa(i)) // clone the slice as we will append an index
	}
	path := stripIdxFromLastPathElm(fullPath.Path)
	if !addedNew { // cleanup old references since the value is being overwritten
		onm.removeRef(path)
	}
	for _, pathItms := range pathItmsSet { // add the path in order list
		onm.orderRef[path] = append(onm.orderRef[path], onm.orderIdx.PushBack(pathItms))
	}
	return
}

// FieldAsString returns the value from path as string
func (onm *OrderedNavigableMap) FieldAsString(fldPath []string) (str string, err error) {
	var val any
	if val, err = onm.FieldAsInterface(fldPath); err != nil {
		return
	}
	return IfaceAsString(val), nil
}

// FieldAsInterface returns the interface at the path
func (onm *OrderedNavigableMap) FieldAsInterface(fldPath []string) (iface any, err error) {
	return onm.nm.FieldAsInterface(fldPath)
}

// GetOrder returns the elements order as a slice
func (onm *OrderedNavigableMap) GetOrder() (order [][]string) {
	for el := onm.GetFirstElement(); el != nil; el = el.Next() {
		order = append(order, el.Value)
	}
	return
}

// OrderedFields returns the elements in order they were inserted
func (onm *OrderedNavigableMap) OrderedFields() (flds []any) {
	flds = make([]any, 0, len(onm.nm.Map))
	for el := onm.GetFirstElement(); el != nil; el = el.Next() {
		fld, _ := onm.Field(el.Value)
		flds = append(flds, fld.Data)
	}
	return
}

// RemoveAll will clean the data and the odrder from OrderedNavigableMap
func (onm *OrderedNavigableMap) RemoveAll() {
	onm.nm = &DataNode{Type: NMMapType, Map: make(map[string]*DataNode)}
	onm.orderIdx = NewPathItemList()
	onm.orderRef = make(map[string][]*PathItemElement)
}

// OrderedFieldsAsStrings returns the elements as strings in order they were inserted
func (onm *OrderedNavigableMap) OrderedFieldsAsStrings() (flds []string) {
	flds = make([]string, 0, len(onm.nm.Map))
	for el := onm.GetFirstElement(); el != nil; el = el.Next() {
		fld, _ := onm.Field(el.Value)
		flds = append(flds, fld.String())
	}
	return
}

// Append appends the leaf at the given path the end must be a slice
// this used with full path and the processed path to not calculate them for every set
func (onm *OrderedNavigableMap) Append(fullPath *FullPath, val *DataLeaf) (err error) {
	if fullPath == nil || len(fullPath.PathSlice) == 0 {
		return ErrWrongPath
	}
	var idx int
	if idx, err = onm.nm.Append(fullPath.PathSlice, val); err != nil {
		return
	}
	// add the path to order
	onm.orderRef[fullPath.Path] = append(onm.orderRef[fullPath.Path],
		onm.orderIdx.PushBack(
			append(slices.Clone(fullPath.PathSlice), // clone the slice as we will append an index
				strconv.Itoa(idx))))
	return
}

// Compose compose the value of the leaf at the given path
// this used with full path and the processed path to not calculate them for every set
func (onm *OrderedNavigableMap) Compose(fullPath *FullPath, val *DataLeaf) (err error) {
	if fullPath == nil || len(fullPath.PathSlice) == 0 {
		return ErrWrongPath
	}
	if err = onm.nm.Compose(fullPath.PathSlice, val); err != nil {
		return
	}

	if _, hasRef := onm.orderRef[fullPath.Path]; !hasRef { // the element is new so append to order
		onm.orderRef[fullPath.Path] = append(onm.orderRef[fullPath.Path],
			onm.orderIdx.PushBack(
				append(slices.Clone(fullPath.PathSlice), // clone the slice as we will append an index
					"0")))
	} else { // move element in the back of order list
		onm.orderIdx.MoveToBack(onm.orderRef[fullPath.Path][len(onm.orderRef[fullPath.Path])-1])
	}
	return
}
